diff --git a/doc/equations/harmonic_oscillator.png b/doc/equations/harmonic_oscillator.png new file mode 100644 index 0000000000..427a3ea508 Binary files /dev/null and b/doc/equations/harmonic_oscillator.png differ diff --git a/doc/equations/harmonic_oscillator.svg b/doc/equations/harmonic_oscillator.svg new file mode 100644 index 0000000000..7320acb737 --- /dev/null +++ b/doc/equations/harmonic_oscillator.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/html/quadrature.html b/doc/html/quadrature.html index 9041b488ae..dadad954c4 100644 --- a/doc/html/quadrature.html +++ b/doc/html/quadrature.html @@ -51,6 +51,7 @@
Fourier Integrals
Naive Monte Carlo Integration
+
Symplectic Integration
Wavelet Transforms
Numerical Differentiation
Automatic Differentiation
diff --git a/doc/math.qbk b/doc/math.qbk index f48c87b88c..7e46490c61 100644 --- a/doc/math.qbk +++ b/doc/math.qbk @@ -772,6 +772,7 @@ and as a CD ISBN 0-9504833-2-X 978-0-9504833-2-0, Classification 519.2-dc22. [include quadrature/double_exponential.qbk] [include quadrature/ooura_fourier_integrals.qbk] [include quadrature/naive_monte_carlo.qbk] +[include quadrature/symplectic.qbk] [include quadrature/wavelet_transforms.qbk] [include differentiation/numerical_differentiation.qbk] [include differentiation/autodiff.qbk] diff --git a/doc/quadrature/symplectic.qbk b/doc/quadrature/symplectic.qbk new file mode 100644 index 0000000000..1765a3482d --- /dev/null +++ b/doc/quadrature/symplectic.qbk @@ -0,0 +1,103 @@ +[/ +Copyright (c) 2026 Jacob Hass +Use, modification and distribution are subject to the +Boost Software License, Version 1.0. (See accompanying file +LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +] + +[section:symplectic Symplectic Integration] + +[heading Synopsis] + + #include + namespace boost{ namespace math{ namespace quadrature { + + template + std::pair, std::vector > integrate_hamiltonian(const ReturnType p0, + const ReturnType q0, + const RealType dt, + const unsigned steps, + Func dHdp, + Func dHdq, + std::string method = "Y6") + + template + std::pair, std::vector > integrate_hamiltonian(const ReturnType p0, + const ReturnType q0, + const RealType dt, + const unsigned steps, + Func dHdp, + Func dHdq, + std::string method, + const ``__Policy``& pol) + + }}} // namespaces + +[heading Description] + +The functional `integrate_hamiltonian` calculates the phase space trajectory for a given Hamiltonian. +The trajectories are calculated using symplectic integration which preserves the energy of +a system. Even higher order traditional numerical integration algorithms will gain or lose +energy at long times. The functional implements the methods in [@https://www.sciencedirect.com/science/article/abs/pii/0375960190900923 Yoshida's] +landmark paper. We assume that the Hamiltonian is separable so that it can be written as `H = T(p) + V(q)`. + +We now give an example for a simple harmonic oscillator with the Hamiltonian +[/ $H = \frac{p^2}{2m} + \frac{1}{2}kx^2$ ] +[equation harmonic_oscillator] + +For simplicity we will assume `k = m = 1`. Then the partial derivatives of the Hamiltonian +with respect to `p` and `q` are + + double dHdp(double p) + { + return p; + } + + double dHdq(double q) + { + return q; + } + +Note that the functional can be readily generalized to multiple coordinates by changing the +signature of `dHdp` to + + std::valarray dHdp(std::valarray p) + { + // calculate the partial derivatives with respect to each p_i + } + +The function must return a `valarray` type, as opposed to `std::vector`, because we must be +able to perform arithmetic operations on the return type. We then define the timestep size +and number of steps of the algorithm to go from `t=0` to `t=100` + + RealType dt = 0.05; + RealType t_end = 100; + unsigned int steps = t_end / dt; + +Lastly, we define the initial conditions so that the oscillator starts from rest at `x=1` + + RealType q0 = 1; + RealType p0 = 0; + +We now evolve the system using the following + + std::vector p; + std::vector q; + + std::tie(p, q) = boost::math::quadrature::integrate_hamiltonian(p0, q0, dt, steps, dHdp, dHdq, "Y6"); + +The ouput vectors `q, p` are the position and momentum of the system at each time. In +higher dimensions the output will be a vector of vectors. + +The last argument that we pass to `integrate_hamiltonian`, "Y6" here, sets the integration +method to use. The string "Y6" stands for Yoshida's 6th order integrator. Yoshida's 2nd and +4th order methods are also available by passing the string "Y2", or "Y4" respectively. + +[optional_policy] + +References: + +Yoshida, Haruo. [`Construction of higher order symplectic integrators], Physics Letters A, 150.5-7 (1990): 262-268. + +[endsect] [/section:symplectic Symplectic Integration] + diff --git a/include/boost/math/quadrature/symplectic.hpp b/include/boost/math/quadrature/symplectic.hpp new file mode 100644 index 0000000000..b71558b501 --- /dev/null +++ b/include/boost/math/quadrature/symplectic.hpp @@ -0,0 +1,175 @@ +// Copyright Jacob Hass, 2026 +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_MATH_QUADRATURE_SYMPLECTIC_HPP +#define BOOST_MATH_QUADRATURE_SYMPLECTIC_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost{ namespace math {namespace quadrature { + +template +std::pair second_order_yoshida(const ReturnType p0, + const ReturnType q0, + RealType dt, + Func dHdp, + Func dHdq) +{ + ReturnType p = p0; + ReturnType q = q0; + + // Half step in q + auto dHdp_val = dHdp(p); + for (unsigned i=0; i < q.size(); i++){ + q[i] = q[i] + dt / 2 * dHdp_val[i]; + } + + // Full step in p + auto dHdq_val = dHdq(q); + for (unsigned i=0; i < p.size(); i++){ + p[i] = p[i] - dt * dHdq_val[i]; + } + + // Half step in q + dHdp_val = dHdp(p); + for (unsigned i=0; i < q.size(); i++){ + q[i] = q[i] + dt / 2 * dHdp_val[i]; + } + + return std::make_pair(p, q); +} + +template +std::pair fourth_order_yoshida(const ReturnType p0, + const ReturnType q0, + const RealType dt, + Func dHdp, + Func dHdq) +{ + ReturnType p = p0; + ReturnType q = q0; + + RealType x0 = -(std::pow(2, 1/3) / (2 - std::pow(2, 1/3))); + RealType x1 = 1 / (2 - std::pow(2, 1/3)); + + std::vector weights = { x1, x0, x1 }; + + for (unsigned i=0; i < weights.size(); i++) + { + std::tie(p, q) = second_order_yoshida(p, q, weights[i] * dt, dHdp, dHdq); + } + + return std::make_pair(p, q); +} + +template +std::pair sixth_order_yoshida(const ReturnType p0, + const ReturnType q0, + RealType dt, + Func dHdp, + Func dHdq) +{ + ReturnType p = p0; + ReturnType q = q0; + + // Choosing "System A" solution + // The following Mathematica command can calculate these coefficients to arbitrary precision + // FindRoot[{w0+2(w1+w2+w3)==1, + // w0^3 + 2(w1^3 + w2^3+w3^3)==0, + // w0^5 + 2(w1^5 + w2^5+w3^5)==0, + // 1/6(w0^2w1^3-w0^4*w1) - 1/6(w0^3w1^2-w0*w1^4)+1/6((w0+2w1)^2w2^3-(w0+2w1)(w0^3+2w1^3)w2) - 1/6((w0^3+2w1^3)w2^2-(w0+2w1)w2^4) +1/6((w0+2w1+2w2)^2w3^3-(w0+2w1+2w2)(w0^3+2w1^3+2w2^3)w3)-1/6((w0^3+2w1^3+2w2^3)w3^2-(w0+2w1+2w2)w3^4)==0}, {{w0,1.3151863206839063}, {w1,-1.17767998417887}, {w2,0.235573213359357}, {w3,0.784513610477560}}, WorkingPrecision->64] + RealType w1 = static_cast(-1.17767998417887100694641568096431573463926925263459848447536851379674155618156L); + RealType w2 = static_cast(0.23557321335935813368479318297853460168646808210340111900349313095621471215223L); + RealType w3 = static_cast(0.78451361047755726381949763386634987577682441745149338456794779895125997479548L); + // w0 = 1.31518632068391121888424972823886251435195350615940796180785516777853373846773 + RealType w0 = 1 - 2 * (w1 + w2 + w3); + std::vector weights = { w3, w2, w1, w0, w1, w2, w3}; + + for (unsigned i=0; i < weights.size(); i++) + { + std::tie(p, q) = second_order_yoshida(p, q, weights[i] * dt, dHdp, dHdq); + } + + return std::make_pair(p, q); +} + +template +std::pair, std::vector > integrate_hamiltonian(const ReturnType p0, + const ReturnType q0, + const RealType dt, + const unsigned steps, + Func dHdp, + Func dHdq, + std::string method, + const Policy& pol) +{ + // Not sure how to make this function string nicer + static const char* function = "boost::math::quadrature::integrate_hamiltonian(p0, q0, %1%, steps, dHdp, dHdq)"; + + if ((dt <= 0) || !(boost::math::isfinite)(dt)) + { + RealType val = (boost::math::policies::raise_domain_error(function, "Time step must be positive and finite but got: dt = %1%.\n", dt, pol)); + std::vector nan_vec = {ReturnType(val)}; + return std::make_pair(nan_vec, nan_vec); + } + + // Check if method is available + std::vector available_methods = {"Y6", "Y4", "Y2"}; + + typedef std::pair (*stepperType)(ReturnType, ReturnType, RealType, Func, Func); + + std::map m{{"Y6", sixth_order_yoshida}, + {"Y4", fourth_order_yoshida}, + {"Y2", second_order_yoshida}}; + stepperType stepper = m.at(method); + + std::vector p(steps); + std::vector q(steps); + p[0] = p0; + q[0] = q0; + + ReturnType p_current = p0; + ReturnType q_current = q0; + for (unsigned i=1; i < steps; i++) + { + std::tie(p_current, q_current) = stepper(p_current, q_current, dt, dHdp, dHdq); + p[i] = p_current; + q[i] = q_current; + } + return std::make_pair(p, q); +} + +template +std::pair, std::vector > integrate_hamiltonian(const ReturnType p0, + const ReturnType q0, + const RealType dt, + const unsigned steps, + Func dHdp, + Func dHdq, + std::string method) +{ + return integrate_hamiltonian(p0, q0, dt, steps, dHdp, dHdq, method, boost::math::policies::policy<>()); +} + +template +std::pair, std::vector > integrate_hamiltonian(const ReturnType p0, + const ReturnType q0, + const RealType dt, + const unsigned steps, + Func dHdp, + Func dHdq) +{ + return integrate_hamiltonian(p0, q0, dt, steps, dHdp, dHdq, "Y6", boost::math::policies::policy<>()); +} +}}} +#endif diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 5b67e474df..694ccefdde 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -1341,6 +1341,7 @@ test-suite quadrature : [ run test_trapezoidal.cpp /boost/test//boost_unit_test_framework : : : release [ requires cxx11_lambdas cxx11_auto_declarations cxx11_decltype cxx11_unified_initialization_syntax cxx11_variadic_templates ] $(float128_type) ] + [ run test_symplectic.cpp ] ; test-suite autodiff : diff --git a/test/test_symplectic.cpp b/test/test_symplectic.cpp new file mode 100644 index 0000000000..a1088ef2b9 --- /dev/null +++ b/test/test_symplectic.cpp @@ -0,0 +1,83 @@ +/* + * Copyright Jacob Hass, 2026 + * Use, modification and distribution are subject to the + * Boost Software License, Version 1.0. (See accompanying file + * LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#define BOOST_TEST_MODULE symplectic_quadrature + +#include +#include +#include +#include +#include +#include +#include +#ifdef BOOST_HAS_FLOAT128 +#include +#endif + +#if __has_include() +# include +#endif + +using boost::math::quadrature::integrate_hamiltonian; + +// Equations of motion for simple harmonic oscillator +template +std::vector vector_dHdp(std::vector p) { return p; } +template +std::vector vector_dHdq(std::vector q) { return q; } + +template +void test_invalid_parameters() +{ + std::vector q0 = {1}; + std::vector p0 = {0}; + // Negative timestep + BOOST_CHECK_THROW(boost::math::quadrature::integrate_hamiltonian(q0, p0, -0.1, 10, vector_dHdp, vector_dHdq), std::domain_error); + + // Method not in {'Y6', 'Y4', 'Y2'} + BOOST_CHECK_THROW(boost::math::quadrature::integrate_hamiltonian(q0, p0, 0.1, 10, vector_dHdp, vector_dHdq, "InvalidMethod"), std::out_of_range); +} + +/* Test if SHO energy fluctuations are below a given tolerance*/ +template +void test_harmonic_oscillator(RealType tol) +{ + BOOST_MATH_STD_USING; + + RealType dt = 0.05; + RealType t_end = 100; + unsigned int steps = t_end / dt; + + std::vector q0 = {1}; + std::vector p0 = {0}; + + std::vector > p; + std::vector > q; + + std::tie(p, q) = boost::math::quadrature::integrate_hamiltonian(p0, q0, dt, steps, vector_dHdp, vector_dHdp); + + RealType p_val; + RealType q_val; + std::vector abs_energy_error(p.size()); + for (unsigned i=0; i < p.size(); i++) + { + p_val = p[i][0]; + q_val = q[i][0]; + + abs_energy_error[i] = std::abs(std::pow(p_val, 2) + std::pow(q_val, 2) - 1); + } + + RealType max_error = *std::max_element(std::begin(abs_energy_error), std::end(abs_energy_error)); + std::cout << max_error << std::endl; + BOOST_CHECK_LE(max_error, tol); + +} + +BOOST_AUTO_TEST_CASE(symplectic_quadrature) +{ + test_invalid_parameters(); + test_harmonic_oscillator(1e-10); +}