diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index af33f1d7a35458a71d4ad7159f9621e2ecfe0ccf..e672c5036ced824ff025be7bb2c1686536febdd1 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -4,6 +4,7 @@ set(TESTS cosseratrodenergytest cosseratrodtest embeddedglobalgfefunctiontest + interpolationderivativestest localadolcstiffnesstest localgeodesicfefunctiontest localgfetestfunctiontest diff --git a/test/interpolationderivativestest.cc b/test/interpolationderivativestest.cc new file mode 100644 index 0000000000000000000000000000000000000000..eae3578988aac3b4e5bc60952cbada448213e222 --- /dev/null +++ b/test/interpolationderivativestest.cc @@ -0,0 +1,572 @@ +/** \file + \brief Unit tests for classes that implement derivatives of interpolation functions + */ +#include <config.h> + +#define DUNE_ISTL_WITH_CHECKING + +#include <adolc/adolc.h> +#include <dune/fufem/utilities/adolcnamespaceinjections.hh> + +#include <dune/common/test/testsuite.hh> + +#include <dune/grid/uggrid.hh> + +#include <dune/istl/io.hh> + +#include <dune/functions/functionspacebases/lagrangebasis.hh> + +#include <dune/gfe/spaces/unitvector.hh> +#include <dune/gfe/spaces/realtuple.hh> + +#include <dune/gfe/interpolationderivatives.hh> + +#include "valuefactory.hh" + + + +using namespace Dune; + +/** \brief Compute derivatives of GFE interpolation with respect to the coefficients using finite differencts + * + * This class implements the InterpolationDerivatives interface but uses a finite difference + * approximation to approximate those derivatives. This is used for testing purposes only. + * + * \tparam LocalInterpolationRule The class that implements the interpolation from a set of coefficients + * + */ +template <typename LocalInterpolationRule> +class FiniteDifferenceInterpolationDerivatives +{ + using TargetSpace = typename LocalInterpolationRule::TargetSpace; + using Derivative = typename LocalInterpolationRule::DerivativeType; + + constexpr static auto blocksize = TargetSpace::TangentVector::dimension; + constexpr static auto embeddedBlocksize = TargetSpace::EmbeddedTangentVector::dimension; + + ////////////////////////////////////////////////////////////////////// + // Data members + ////////////////////////////////////////////////////////////////////// + + // TODO: Do not hard-wirde this! + static constexpr int domainDim = 2; + + FieldVector<double,domainDim> localPos_; + FieldMatrix<double,domainDim,domainDim> geometryJacobianInverse_; + + const LocalInterpolationRule& localInterpolationRule_; + + std::vector<TargetSpace> coefficients_; + + // TODO: Don't hardcode FieldMatrix + std::vector<FieldMatrix<double,blocksize,embeddedBlocksize> > orthonormalFrames_; + +public: + + FiniteDifferenceInterpolationDerivatives(const LocalInterpolationRule& localInterpolationRule) + : localInterpolationRule_(localInterpolationRule) + { + // Copy the coefficients into a dedicated array, for easier access + coefficients_.resize(localInterpolationRule.size()); + for (std::size_t i=0; i<localInterpolationRule.size(); i++) + coefficients_[i] = localInterpolationRule.coefficient(i); + + // Precompute the orthonormal frames + orthonormalFrames_.resize(localInterpolationRule_.size()); + for (size_t i=0; i<localInterpolationRule_.size(); ++i) + orthonormalFrames_[i] = localInterpolationRule_.coefficient(i).orthonormalFrame(); + } + + /** \brief Bind the objects to a particular evaluation point + * + * In particular, this computes the value of the interpolation function at that point, + * and the derivative at that point with respect to space. + * + * \param[in] tapeNumber Number of the ADOL-C tape if ADOL-C is used. Dummy otherwise + * \param[in] localPos Local position where the FE function is evaluated + * \param[out] value The function value at the local configuration + * \param[out] derivative The derivative of the interpolation function + * with respect to the evaluation point + */ + template <typename Element> + void bind(short tapeNumber, + const Element& element, + const typename Element::Geometry::LocalCoordinate& localPos, + typename TargetSpace::CoordinateType& value, + typename LocalInterpolationRule::DerivativeType& derivative) + { + localPos_ = localPos; + + value = localInterpolationRule_.evaluate(localPos).globalCoordinates(); + + geometryJacobianInverse_ = element.geometry().jacobianInverse(localPos); + + auto referenceDerivative = localInterpolationRule_.evaluateDerivative(localPos, value); + derivative = referenceDerivative * geometryJacobianInverse_; + } + + /** \brief Compute first and second derivatives of the FE interpolation + * + * This code assumes that `bind` has been called before. + * + * \param[in] tapeNumber The tape number to be used by ADOL-C. Must be the same + * that was given to the `bind` method. + * \param[in] weights Vector of weights that the second derivative is contracted with + * \param[out] embeddedFirstDerivative Derivative of the FE interpolation wrt the coefficients + * \param[out] firstDerivative Derivative of the FE interpolation wrt the coefficients + * \param[out] secondDerivative Second derivative of the FE interpolation, + * contracted with the weight vector + */ + void evaluateDerivatives(short tapeNumber, + const std::vector<double>& adjoint, + Matrix<double>& euclideanFirstDerivative, + Matrix<double>& riemannianFirstDerivative, + Matrix<FieldMatrix<double,blocksize,blocksize> >& secondDerivative) const + { + //////////////////////////////////////////////////////////////////////// + // Compute Euclidean first derivative of the interpolation value + //////////////////////////////////////////////////////////////////////// + + for (std::size_t coefficient=0; coefficient<localInterpolationRule_.size(); coefficient++) + { + std::vector<TargetSpace> cornersPlus = coefficients_; + std::vector<TargetSpace> cornersMinus = coefficients_; + + for (std::size_t j=0; j<TargetSpace::CoordinateType::size(); j++) + { + // Optimal variation size for first derivatives + const double eps = std::sqrt(std::numeric_limits<double>::epsilon()); + + // Variation in coordinates of the surrounding spaces + typename TargetSpace::CoordinateType variation(0.0); + variation[j] = eps; + + cornersPlus [coefficient] = TargetSpace(coefficients_[coefficient].globalCoordinates() + variation); + cornersMinus[coefficient] = TargetSpace(coefficients_[coefficient].globalCoordinates() - variation); + + LocalInterpolationRule fPlus(localInterpolationRule_.localFiniteElement(),cornersPlus); + LocalInterpolationRule fMinus(localInterpolationRule_.localFiniteElement(),cornersMinus); + + ///////////////////////////////////////////////////////////// + // Compute first derivative of the interpolation value + ///////////////////////////////////////////////////////////// + + TargetSpace hPlus = fPlus.evaluate(localPos_); + TargetSpace hMinus = fMinus.evaluate(localPos_); + + for (std::size_t k=0; k<TargetSpace::CoordinateType::size(); k++) + euclideanFirstDerivative[k][coefficient*TargetSpace::CoordinateType::size()+j] + = (hPlus.globalCoordinates()[k] - hMinus.globalCoordinates()[k]) / (2*eps); + + ///////////////////////////////////////////////////////////// + // Compute first derivative of the interpolation gradient + ///////////////////////////////////////////////////////////// + auto hPlusDer = fPlus.evaluateDerivative(localPos_) * geometryJacobianInverse_; + auto hMinusDer = fMinus.evaluateDerivative(localPos_) * geometryJacobianInverse_; + + for (std::size_t k=0; k<hPlusDer.N(); k++) + for (std::size_t l=0; l<hPlusDer.M(); l++) + euclideanFirstDerivative[k*hPlusDer.M()+l+TargetSpace::CoordinateType::size()][coefficient*TargetSpace::CoordinateType::size()+j] = (hPlusDer[k][l] - hMinusDer[k][l]) / (2*eps); + } + } + + + //////////////////////////////////////////////////////////////////////// + // Compute Riemannian first derivative of the interpolation value + //////////////////////////////////////////////////////////////////////// + + for (std::size_t coefficient=0; coefficient<localInterpolationRule_.size(); coefficient++) + { + // the function value at the point where we are evaluating the derivative + const auto B = orthonormalFrames_[coefficient]; + + std::vector<TargetSpace> cornersPlus = coefficients_; + std::vector<TargetSpace> cornersMinus = coefficients_; + + for (std::size_t j=0; j<B.size(); j++) + { + // Optimal variation size for first derivatives + const double eps = std::sqrt(std::numeric_limits<double>::epsilon()); + + auto forwardVariation = B[j]; + forwardVariation *= eps; + auto backwardVariation = B[j]; + backwardVariation *= -eps; + + cornersPlus [coefficient] = TargetSpace::exp(coefficients_[coefficient], forwardVariation); + cornersMinus[coefficient] = TargetSpace::exp(coefficients_[coefficient], backwardVariation); + + LocalInterpolationRule fPlus(localInterpolationRule_.localFiniteElement(),cornersPlus); + LocalInterpolationRule fMinus(localInterpolationRule_.localFiniteElement(),cornersMinus); + + ///////////////////////////////////////////////////////////// + // Compute first derivative of the interpolation value + ///////////////////////////////////////////////////////////// + + TargetSpace hPlus = fPlus.evaluate(localPos_); + TargetSpace hMinus = fMinus.evaluate(localPos_); + + for (std::size_t k=0; k<TargetSpace::CoordinateType::size(); k++) + riemannianFirstDerivative[k][coefficient*B.size()+j] + = (hPlus.globalCoordinates()[k] - hMinus.globalCoordinates()[k]) / (2*eps); + + ///////////////////////////////////////////////////////////// + // Compute first derivative of the interpolation gradient + ///////////////////////////////////////////////////////////// + auto hPlusDer = fPlus.evaluateDerivative(localPos_) * geometryJacobianInverse_; + auto hMinusDer = fMinus.evaluateDerivative(localPos_) * geometryJacobianInverse_; + + for (std::size_t k=0; k<hPlusDer.N(); k++) + for (std::size_t l=0; l<hPlusDer.M(); l++) + riemannianFirstDerivative[k*hPlusDer.M()+l+TargetSpace::CoordinateType::size()][coefficient*B.size()+j] = (hPlusDer[k][l] - hMinusDer[k][l]) / (2*eps); + } + } + + + /////////////////////////////////////////////////////////////////////////// + // Compute Riemannian Hesse matrix by finite-difference approximation. + /////////////////////////////////////////////////////////////////////////// + + // Precompute value at the current configuration + auto centerValue = localInterpolationRule_.evaluate(localPos_).globalCoordinates(); + auto centerDerivative = localInterpolationRule_.evaluateDerivative(localPos_)* geometryJacobianInverse_; + + // Precompute energy infinitesimal corrections in the directions of the local basis vectors + std::vector<std::array<TargetSpace,blocksize> > forwardValue(coefficients_.size()); + std::vector<std::array<TargetSpace,blocksize> > backwardValue(coefficients_.size()); + + std::vector<std::array<Derivative,blocksize> > forwardDer(coefficients_.size()); + std::vector<std::array<Derivative,blocksize> > backwardDer(coefficients_.size()); + + BlockVector<FieldVector<double,blocksize> > canonicalValues(coefficients_.size()); + + for (size_t i=0; i<coefficients_.size(); i++) + { + for (size_t i2=0; i2<blocksize; i2++) + { + // Optimal variation size for second derivatives + const double eps = std::pow(std::numeric_limits<double>::epsilon(), 0.25); + + typename TargetSpace::EmbeddedTangentVector xi = orthonormalFrames_[i][i2]; + + auto forwardSolution = coefficients_; + auto backwardSolution = coefficients_; + + forwardSolution[i] = TargetSpace::exp(coefficients_[i], eps * xi); + backwardSolution[i] = TargetSpace::exp(coefficients_[i], -1 * eps * xi); + + LocalInterpolationRule fPlus(localInterpolationRule_.localFiniteElement(),forwardSolution); + LocalInterpolationRule fMinus(localInterpolationRule_.localFiniteElement(),backwardSolution); + + forwardValue[i][i2] = fPlus.evaluate(localPos_); + backwardValue[i][i2] = fMinus.evaluate(localPos_); + + forwardDer[i][i2] = fPlus.evaluateDerivative(localPos_)* geometryJacobianInverse_; + backwardDer[i][i2] = fMinus.evaluateDerivative(localPos_)* geometryJacobianInverse_; + + // Finite difference quotient for the second derivative + auto valueDerivative = (forwardValue[i][i2].globalCoordinates() -2*centerValue + backwardValue[i][i2].globalCoordinates()) / (eps * eps); + + auto jacobianDerivative = (forwardDer[i][i2] -2*centerDerivative + backwardDer[i][i2]) / (eps * eps); + + // Multiply with the adjoint + canonicalValues[i][i2] = 0; + for (std::size_t j=0; j<valueDerivative.size(); j++) + canonicalValues[i][i2] += adjoint[j] * valueDerivative[j]; + + for (std::size_t j=0; j<jacobianDerivative.N(); j++) + for (std::size_t j2=0; j2<jacobianDerivative.M(); j2++) + canonicalValues[i][i2] += adjoint[valueDerivative.size() + j*jacobianDerivative.M() + j2] * jacobianDerivative[j][j2]; + } + } + + for (size_t i=0; i<localInterpolationRule_.size(); i++) + { + for (size_t i2=0; i2<blocksize; i2++) + { + for (size_t j=0; j<localInterpolationRule_.size(); j++) + { + for (size_t j2=0; j2<blocksize; j2++) + { + // Optimal variation size for second derivatives + const double eps = std::pow(std::numeric_limits<double>::epsilon(), 0.25); + + std::vector<TargetSpace> forwardSolutionXiEta = coefficients_; + std::vector<TargetSpace> backwardSolutionXiEta = coefficients_; + + typename TargetSpace::EmbeddedTangentVector epsXi = orthonormalFrames_[i][i2]; + epsXi *= eps; + typename TargetSpace::EmbeddedTangentVector epsEta = orthonormalFrames_[j][j2]; + epsEta *= eps; + + if (i==j) + forwardSolutionXiEta[i] = TargetSpace::exp(coefficients_[i],epsXi+epsEta); + else { + forwardSolutionXiEta[i] = TargetSpace::exp(coefficients_[i],epsXi); + forwardSolutionXiEta[j] = TargetSpace::exp(coefficients_[j],epsEta); + } + + if (i==j) + backwardSolutionXiEta[i] = TargetSpace::exp(coefficients_[i], (-1)*epsXi + (-1)*epsEta); + else { + backwardSolutionXiEta[i] = TargetSpace::exp(coefficients_[i], (-1)*epsXi); + backwardSolutionXiEta[j] = TargetSpace::exp(coefficients_[j], (-1)*epsEta); + } + + LocalInterpolationRule fPlus(localInterpolationRule_.localFiniteElement(),forwardSolutionXiEta); + LocalInterpolationRule fMinus(localInterpolationRule_.localFiniteElement(),backwardSolutionXiEta); + + ///////////////////////////////////////////////////////////////////////////////////// + // Compute second derivative of the adjoint vector times the interpolation value + ///////////////////////////////////////////////////////////////////////////////////// + + auto forwardTmp = fPlus.evaluate(localPos_).globalCoordinates(); + auto backwardTmp = fMinus.evaluate(localPos_).globalCoordinates(); + + auto foo = (forwardTmp - 2*centerValue + backwardTmp) / (eps*eps); + + // Scalar product: ... = adjoint * foo; + secondDerivative[i][j][i2][j2] = 0; + for (std::size_t k=0; k<foo.size(); k++) + secondDerivative[i][j][i2][j2] += adjoint[k] * foo[k]; + + ///////////////////////////////////////////////////////////////////////////////////// + // Compute second derivative of the adjoint vector times the interpolation gradient + ///////////////////////////////////////////////////////////////////////////////////// + + auto forwardDerTmp = fPlus.evaluateDerivative(localPos_)* geometryJacobianInverse_; + auto backwardDerTmp = fMinus.evaluateDerivative(localPos_)* geometryJacobianInverse_; + + auto foo2 = (forwardDerTmp - 2*centerDerivative + backwardDerTmp) / (eps*eps); + // Scalar product: ... += adjoint * foo2; + for (std::size_t k=0; k<foo2.N(); k++) + for (std::size_t l=0; l<foo2.M(); l++) + secondDerivative[i][j][i2][j2] += adjoint[k*foo2.M()+l+TargetSpace::CoordinateType::size()] * foo2[k][l]; + + //////////////////////////////////////////////////////////////////////////////////// + // Use a polarization identity to get the actual Hesse matrix entry + //////////////////////////////////////////////////////////////////////////////////// + + secondDerivative[i][j][i2][j2] = 0.5 * (secondDerivative[i][j][i2][j2] - canonicalValues[i][i2] - canonicalValues[j][j2]); + } + } + } + } + } + +}; + + +enum class InterpolationType {Geodesic, ProjectionBased}; + +template <class TargetSpace, InterpolationType interpolationType> +TestSuite checkDerivatives() +{ + TestSuite test; + + std::cout << "Testing class " << className<TargetSpace>() << std::endl; + + //////////////////////////////////////////////////// + // Make grid consisting of a single triangle + //////////////////////////////////////////////////// + + static const int domainDim = 2; + using Grid = UGGrid<domainDim>; + GridFactory<Grid> factory; + + factory.insertVertex({1.0, 1.0}); + factory.insertVertex({2.0, 1.5}); + factory.insertVertex({2.5, 3.0}); + + factory.insertElement(GeometryTypes::simplex(domainDim), {0,1,2}); + + auto grid = factory.createGrid(); + auto gridView = grid->leafGridView(); + using GridView = decltype(gridView); + + ///////////////////////////////////////////////////////////////////////// + // Construct a LocalInterpolationRule whose derivative we will compute + ///////////////////////////////////////////////////////////////////////// + + constexpr int order = 1; + Functions::LagrangeBasis<GridView,order> scalarBasis(gridView); + + std::vector<TargetSpace> testPoints; + ValueFactory<TargetSpace>::get(testPoints); + + // TODO: Make sure the list of test points is longer than this. + const std::size_t nDofs = scalarBasis.dimension(); + + std::vector<TargetSpace> localCoefficients(nDofs); + for (std::size_t i=0; i<nDofs; i++) + localCoefficients[i] = testPoints[i]; + + ///////////////////////////////////////////////////////////////////////// + // Construct the InterpolationDerivatives object that we will test + ///////////////////////////////////////////////////////////////////////// + + // Define the two possible interpolation rules + using GeodesicInterpolationRule = LocalGeodesicFEFunction<domainDim, + typename Grid::ctype, + decltype(scalarBasis.localView().tree().finiteElement()), + TargetSpace>; + + using ProjectionBasedInterpolationRule = GFE::LocalProjectedFEFunction<domainDim, + typename Grid::ctype, + decltype(scalarBasis.localView().tree().finiteElement()), + TargetSpace>; + + // Select the one to test + using LocalInterpolationRule = std::conditional_t<interpolationType==InterpolationType::Geodesic, + GeodesicInterpolationRule, + ProjectionBasedInterpolationRule>; + + + auto localView = scalarBasis.localView(); + localView.bind(*gridView.begin<0>()); + LocalInterpolationRule localGFEFunction(localView.tree().finiteElement(),localCoefficients); + + GFE::InterpolationDerivatives<LocalInterpolationRule> interpolationDerivatives(localGFEFunction, + true, // doValue + true); // doDerivative + + ///////////////////////////////////////////////////////////////////////// + // Construct the finite difference InterpolationDerivatives object + // that we will use to compare with + ///////////////////////////////////////////////////////////////////////// + + FiniteDifferenceInterpolationDerivatives<LocalInterpolationRule> interpolationDerivativesFD(localGFEFunction); + + ///////////////////////////////////////////////////////////////////////// + // Bind the two objects to a test point, and verify that this + // produces identical results. + ///////////////////////////////////////////////////////////////////////// + + // InterpolationDerivatives uses ADOL-C by default. Therefore, give a tape number + const int tapeNumber = 0; + + const typename Grid::template Codim<0>::Entity::Geometry::LocalCoordinate position = {0.3, 0.3}; + + typename TargetSpace::CoordinateType valueGlobalCoordinates; + typename TargetSpace::CoordinateType valueFDGlobalCoordinates; + + typename LocalInterpolationRule::DerivativeType derivative; + typename LocalInterpolationRule::DerivativeType derivativeFD; + + interpolationDerivatives.bind(tapeNumber, + localView.element(), + position, + valueGlobalCoordinates, + derivative); + + TargetSpace value(valueGlobalCoordinates); + + interpolationDerivativesFD.bind(tapeNumber, + localView.element(), + position, + valueFDGlobalCoordinates, + derivativeFD); + + TargetSpace valueFD(valueFDGlobalCoordinates); + + /////////////////////////////////////////////////////// + // Compute the derivatives, and compare them + /////////////////////////////////////////////////////// + + constexpr auto blocksize = TargetSpace::TangentVector::dimension; + constexpr auto embeddedBlocksize = TargetSpace::EmbeddedTangentVector::dimension; + // Number of dependent variables for the interpolation function + // The sum of the variables for the interpolation value and the variables + // for the derivative + constexpr auto m = TargetSpace::CoordinateType::size() + embeddedBlocksize*domainDim; + + std::vector<double> weights(m); + + for (std::size_t i=0; i<m; i++) + { + std::fill(weights.begin(), weights.end(), 0.0); + + weights[i] = 1.0; + + + Matrix<double> euclideanInterpolationGradient(m, nDofs*embeddedBlocksize); + Matrix<double> riemannianInterpolationGradient(m, nDofs*blocksize); + + Matrix<FieldMatrix<double,blocksize,blocksize> > interpolationHessian(nDofs,nDofs); + + + interpolationDerivatives.evaluateDerivatives(tapeNumber, + weights.data(), + euclideanInterpolationGradient, + riemannianInterpolationGradient, + interpolationHessian); + + Matrix<double> euclideanInterpolationGradientFD(m, nDofs*embeddedBlocksize); + Matrix<double> riemannianInterpolationGradientFD(m, nDofs*blocksize); + + + Matrix<FieldMatrix<double,blocksize,blocksize> > interpolationHessianFD(nDofs,nDofs); + + interpolationDerivativesFD.evaluateDerivatives(tapeNumber, + weights, + euclideanInterpolationGradientFD, + riemannianInterpolationGradientFD, + interpolationHessianFD); + + ///////////////////////////////////////////////////////////////// + // Compare the derivatives + ///////////////////////////////////////////////////////////////// + + auto riemannianDifference = riemannianInterpolationGradient; + riemannianDifference -= riemannianInterpolationGradientFD; + + if (std::isnan(riemannianDifference.infinity_norm()) || riemannianDifference.infinity_norm() > 1e-6) + { + printmatrix(std::cout, riemannianInterpolationGradient, "riemannianInterpolationGradient", "--"); + printmatrix(std::cout, riemannianInterpolationGradientFD, "riemannianInterpolationGradientFD", "--"); + } + + auto euclideanDifference = euclideanInterpolationGradient; + euclideanDifference -= euclideanInterpolationGradientFD; + + if (std::isnan(euclideanDifference.infinity_norm()) || euclideanDifference.infinity_norm() > 1e-6) + { + printmatrix(std::cout, euclideanInterpolationGradient, "euclideanInterpolationGradient", "--"); + printmatrix(std::cout, euclideanInterpolationGradientFD, "euclideanInterpolationGradientFD", "--"); + } + + auto hessianDifference = interpolationHessian; + hessianDifference -= interpolationHessianFD; + + if (std::isnan(hessianDifference.infinity_norm()) || hessianDifference.infinity_norm() > 1e-5) + { + printmatrix(std::cout, interpolationHessian, "interpolationHessian", "--"); + printmatrix(std::cout, interpolationHessianFD, "interpolationHessianFD", "--"); + } + } + return test; +} + + +int main (int argc, char *argv[]) +{ + // Set up MPI, if available + MPIHelper::instance(argc, argv); + + TestSuite test; + + // Test the UnitSphere class and geodesic interpolation. + // This uses the default derivatives implementation (using ADOL-C) + test.subTest(checkDerivatives<UnitVector<double,3>, InterpolationType::Geodesic >()); + + // Test the RealTuple class, both with geodesic and projection-based interpolation + // Both are specialized + test.subTest(checkDerivatives<RealTuple<double,3>, InterpolationType::Geodesic>()); + test.subTest(checkDerivatives<RealTuple<double,3>, InterpolationType::ProjectionBased>()); + + // Test the UnitVector class with projection-based interpolation + // This is also specialized. + test.subTest(checkDerivatives<UnitVector<double,3>, InterpolationType::ProjectionBased>()); + + return test.exit(); +}