ampsci
c++ program for high-precision atomic structure calculations of single-valence systems
AdamsMoulton.hpp
1 #pragma once
2 #include <algorithm>
3 #include <array>
4 #include <cassert>
5 #include <complex>
6 #include <tuple>
7 #include <type_traits>
8 
11 namespace AdamsMoulton {
12 
13 //==============================================================================
14 
18 
78 template <typename T = double, typename Y = double>
81  virtual Y a(T t) const = 0;
82  virtual Y b(T t) const = 0;
83  virtual Y c(T t) const = 0;
84  virtual Y d(T t) const = 0;
86  virtual Y Sf(T) const { return Y(0); };
87  virtual Y Sg(T) const { return Y(0); };
88  virtual ~DerivativeMatrix() = default;
89 };
90 
91 //==============================================================================
92 
93 // User-defined type-trait: Checks whether T is a std::complex type
94 template <typename T>
95 struct is_complex : std::false_type {};
96 // User-defined type-trait: Checks whether T is a std::complex type
97 template <typename T>
98 struct is_complex<std::complex<T>> : std::true_type {};
100 
108 template <typename T>
109 constexpr bool is_complex_v = is_complex<T>::value;
110 
111 //==============================================================================
112 
114 
121 template <typename T, typename U, std::size_t N, std::size_t M>
122 constexpr T inner_product(const std::array<T, N> &a,
123  const std::array<U, M> &b) {
124  static_assert(std::is_convertible_v<U, T>,
125  "In inner_product, type of second array (U) must be "
126  "convertable to that of dirst (T)");
127  constexpr std::size_t Size = std::min(N, M);
128 
129  if constexpr (Size == 0) {
130  return T{0};
131  } else if constexpr (!std::is_same_v<T, U> && is_complex_v<T>) {
132  // This is to avoid float conversion warning in case that U=double,
133  // T=complex<float>; want to case b to float, then to complex<float>
134  T res = a[0] * static_cast<typename T::value_type>(b[0]);
135  for (std::size_t i = 1; i < Size; ++i) {
136  res += a[i] * static_cast<typename T::value_type>(b[i]);
137  }
138  return res;
139  } else {
140  T res = a[0] * static_cast<T>(b[0]);
141  for (std::size_t i = 1; i < Size; ++i) {
142  res += a[i] * static_cast<T>(b[i]);
143  }
144  return res;
145  }
146 }
147 
148 //==============================================================================
149 
150 namespace helper {
151 
152 // Simple struct for storing "Raw" Adams ("B") coefficients.
153 /*
154 Stored as integers, with 'denominator' factored out. \n
155 Converted to double ("A" coefs) once, at compile time (see below). \n
156 Adams coefficients, a_k, defined such that: \n
157  \f[ F_{n+K} = F_{n+K-1} + dx * \sum_{k=0}^K a_k y_{n+k} \f]
158 where:
159  \f[ y = d(F)/dr \f]
160 Note: the 'order' of the coefs is reversed compared to some sources.
161 The final coefficient is separated, such that: \n
162  \f[ a_k = b_k / denom \f]
163 for k = {0,1,...,K-1} \n
164 and
165  \f[ a_K = b_K / denom \f]
166 */
167 template <std::size_t K>
168 struct AdamsB {
169  long denom;
170  std::array<long, K> bk;
171  long bK;
172 };
173 
174 // List of Adams coefficients data
175 /*
176 Note: there is (of course) no 0-step Adams method.
177 The entry at [0] is invalid, and will produce 0.
178 It is included so that the index of this list matches the order of the method.
179 Program will not compile (static_asser) is [0] is requested.
180 Note: assumes that the kth element corresponds to k-order AM method.
181 */
182 static constexpr auto ADAMS_data = std::tuple{
183  AdamsB<0>{1, {}, 0}, // invalid entry, but want index to match order
184  AdamsB<1>{2, {1}, 1},
185  AdamsB<2>{12, {-1, 8}, 5},
186  AdamsB<3>{24, {1, -5, 19}, 9},
187  AdamsB<4>{720, {-19, 106, -264, 646}, 251},
188  AdamsB<5>{1440, {27, -173, 482, -798, 1427}, 475},
189  AdamsB<6>{60480, {-863, 6312, -20211, 37504, -46461, 65112}, 19087},
190  AdamsB<7>{
191  120960, {1375, -11351, 41499, -88547, 123133, -121797, 139849}, 36799},
192  AdamsB<8>{3628800,
193  {-33953, 312874, -1291214, 3146338, -5033120, 5595358, -4604594,
194  4467094},
195  1070017},
196  AdamsB<9>{7257600,
197  {57281, -583435, 2687864, -7394032, 13510082, -17283646, 16002320,
198  -11271304, 9449717},
199  2082753},
200  AdamsB<10>{479001600,
201  {-3250433, 36284876, -184776195, 567450984, -1170597042,
202  1710774528, -1823311566, 1446205080, -890175549, 656185652},
203  134211265},
204  AdamsB<11>{958003200,
205  {5675265, -68928781, 384709327, -1305971115, 3007739418,
206  -4963166514, 6043521486, -5519460582, 3828828885, -2092490673,
207  1374799219},
208  262747265},
209  AdamsB<12>{2615348736000,
210  {-13695779093, 179842822566, -1092096992268, 4063327863170,
211  -10344711794985, 19058185652796, -26204344465152,
212  27345870698436, -21847538039895, 13465774256510, -6616420957428,
213  3917551216986},
214  703604254357}};
215 
216 } // namespace helper
217 
218 //==============================================================================
219 
222 static constexpr std::size_t K_max =
223  std::tuple_size_v<decltype(helper::ADAMS_data)> - 1;
224 
225 //==============================================================================
226 
229 
242 template <std::size_t K, typename = std::enable_if_t<(K > 0)>,
243  typename = std::enable_if_t<(K <= K_max)>>
244 struct AM_Coefs {
245 
246 private:
247  // Forms the (double) ak coefficients, from the raw (int) bk ones
248  static constexpr std::array<double, K> make_ak() {
249  const auto &am = std::get<K>(helper::ADAMS_data);
250  static_assert(
251  am.bk.size() == K,
252  "Kth Entry in ADAMS_data must correspond to K-order AM method");
253  std::array<double, K> tak{};
254  for (std::size_t i = 0; i < K; ++i) {
255  tak.at(i) = double(am.bk.at(i)) / double(am.denom);
256  }
257  return tak;
258  }
259 
260  // Forms the final (double) aK coefficient, from the raw (int) bK one
261  static constexpr double make_aK() {
262  const auto &am = std::get<K>(helper::ADAMS_data);
263  return (double(am.bK) / double(am.denom));
264  }
265 
266 public:
268  static constexpr std::array<double, K> ak{make_ak()};
270  static constexpr double aK{make_aK()};
271 };
272 
273 //==============================================================================
275 
435 template <std::size_t K, typename T = double, typename Y = double>
436 class ODESolver2D {
437  static_assert(K > 0, "Order (K) for Adams method must be K>0");
438  static_assert(K <= K_max,
439  "Order (K) requested for Adams method too "
440  "large. Adams coefficients are implemented up to K_max-1 only");
441  static_assert(
442  is_complex_v<Y> || std::is_floating_point_v<Y>,
443  "Template parameter Y (function values and dt) must be floating point "
444  "or complex");
445  static_assert(is_complex_v<Y> || std::is_floating_point_v<Y> ||
446  std::is_integral_v<T>,
447  "Template parameter T (derivative matrix argument) must be "
448  "floating point, complex, or integral");
449 
450 private:
451  // Stores the AM coefficients
452  static constexpr AM_Coefs<K> am{};
453  // step size:
454  Y m_dt;
455  // previous 't' value
456  // T m_t{T(0)}; // Should be invalid (nan), but no nan for int
457  // Pointer to the derivative matrix
458  const DerivativeMatrix<T, Y> *m_D;
459 
460 public:
462 
467  std::array<Y, K> f{}, g{};
468 
470  std::array<Y, K> df{}, dg{};
471 
473  std::array<T, K> t{};
474 
475  Y S_scale{1.0};
476 
477 public:
481 
487  ODESolver2D(Y dt, const DerivativeMatrix<T, Y> *D) : m_dt(dt), m_D(D) {
488  assert(dt != Y{0.0} && "Cannot have zero step-size in ODESolver2D");
489  assert(D != nullptr && "Cannot have null Derivative Matrix in ODESolver2D");
490  }
491 
493  constexpr std::size_t K_steps() const { return K; }
494 
496  Y last_f() { return f.back(); }
498  Y last_g() { return g.back(); }
500  T last_t() { return t.back(); }
502  Y dt() { return m_dt; }
503 
505  Y dfdt(Y ft, Y gt, T tt) const {
506  return m_D->a(tt) * ft + m_D->b(tt) * gt + S_scale * m_D->Sf(tt);
507  }
509  Y dgdt(Y ft, Y gt, T tt) const {
510  return m_D->c(tt) * ft + m_D->d(tt) * gt + S_scale * m_D->Sg(tt);
511  }
512 
515 
527  void drive(T t_next) {
528 
529  // assert (t_next = Approx[next_t(last_t())])
530 
531  // Use AM method to determine new values, given previous K values:
532  const auto sf = f.back() + m_dt * (inner_product(df, am.ak) +
533  am.aK * S_scale * m_D->Sf(t_next));
534  const auto sg = g.back() + m_dt * (inner_product(dg, am.ak) +
535  am.aK * S_scale * m_D->Sg(t_next));
536 
537  const auto a = m_D->a(t_next);
538  const auto b = m_D->b(t_next);
539  const auto c = m_D->c(t_next);
540  const auto d = m_D->d(t_next);
541 
542  const auto a0 = m_dt * static_cast<Y>(am.aK);
543  const auto a02 = a0 * a0;
544  const auto det_inv =
545  Y{1.0} / (Y{1.0} - (a02 * (b * c - a * d) + a0 * (a + d)));
546  const auto fi = (sf - a0 * (d * sf - b * sg)) * det_inv;
547  const auto gi = (sg - a0 * (-c * sf + a * sg)) * det_inv;
548 
549  // Shift values along. nb: rotate({1,2,3,4}) = {2,3,4,1}
550  // We keep track of previous K values in order to determine next value
551  std::rotate(f.begin(), f.begin() + 1, f.end());
552  std::rotate(g.begin(), g.begin() + 1, g.end());
553  std::rotate(df.begin(), df.begin() + 1, df.end());
554  std::rotate(dg.begin(), dg.begin() + 1, dg.end());
555  std::rotate(t.begin(), t.begin() + 1, t.end());
556 
557  // Sets new values:
558  t.back() = t_next;
559  f.back() = fi;
560  g.back() = gi;
561  df.back() = dfdt(fi, gi, t_next);
562  dg.back() = dgdt(fi, gi, t_next);
563  }
564 
568  void drive() { drive(next_t(last_t())); }
569 
572 
581  void solve_initial_K(T t0, Y f0, Y g0) {
582  t.at(0) = t0;
583  f.at(0) = f0;
584  g.at(0) = g0;
585  df.at(0) = dfdt(f0, g0, t0);
586  dg.at(0) = dgdt(f0, g0, t0);
587  first_k_i<1>(next_t(t0));
588  }
589 
590 private:
591  // only used in solve_initial_K(). Use this, because it works for double t,
592  // where next value is t+dt, and for integral t, where next value is t++ is
593  // driving forward (dt>0), or t-- if driving backwards (dt<0)
594  T next_t(T last_t) {
595  if constexpr (std::is_integral_v<T> && is_complex_v<Y>) {
596  return (m_dt.real() > 0.0) ? last_t + 1 : last_t - 1;
597  } else if constexpr (std::is_integral_v<T>) {
598  return (m_dt > 0.0) ? last_t + 1 : last_t - 1;
599  } else {
600  return last_t + m_dt;
601  }
602  }
603 
604  // Used recursively by solve_initial_K() to find first K points
605  template <std::size_t ik>
606  void first_k_i(T t_next) {
607  if constexpr (ik >= K) {
608  (void)t_next; // suppress unused variable warning on old g++ versions
609  return;
610  } else {
611  constexpr AM_Coefs<ik> ai{};
612  // nb: ai.ak is smaller than df; inner_product still works
613  const auto sf = f.at(ik - 1) + m_dt * (inner_product(df, ai.ak) +
614  am.aK * S_scale * m_D->Sf(t_next));
615  const auto sg = g.at(ik - 1) + m_dt * (inner_product(dg, ai.ak) +
616  am.aK * S_scale * m_D->Sg(t_next));
617  const auto a0 = m_dt * static_cast<Y>(ai.aK);
618  const auto a02 = a0 * a0;
619  const auto a = m_D->a(t_next);
620  const auto b = m_D->b(t_next);
621  const auto c = m_D->c(t_next);
622  const auto d = m_D->d(t_next);
623  const auto det_inv =
624  Y{1.0} / (Y{1.0} - (a02 * (b * c - a * d) + a0 * (a + d)));
625  const auto fi = (sf - a0 * (d * sf - b * sg)) * det_inv;
626  const auto gi = (sg - a0 * (-c * sf + a * sg)) * det_inv;
627  // Sets new values:
628  t.at(ik) = t_next;
629  f.at(ik) = fi;
630  g.at(ik) = gi;
631  df.at(ik) = dfdt(fi, gi, t_next);
632  dg.at(ik) = dgdt(fi, gi, t_next);
633  // call recursively
634  first_k_i<ik + 1>(next_t(t_next));
635  }
636  }
637 };
638 } // namespace AdamsMoulton
Solves a 2D system of ODEs using a K-step Adams-Moulton method.
Definition: AdamsMoulton.hpp:436
T last_t()
Returns most recent t value; last_f() := f(last_t())
Definition: AdamsMoulton.hpp:500
Y last_g()
Returns most recent g value. Can also access g array directly.
Definition: AdamsMoulton.hpp:498
std::array< T, K > t
Array to store the previous K values of t: f.at(i) = f(t.at(i))
Definition: AdamsMoulton.hpp:473
constexpr std::size_t K_steps() const
Returns the AM order (number of steps), K.
Definition: AdamsMoulton.hpp:493
void drive(T t_next)
Drives the DE system to next value, F(t), assuming system has already been solved for the K previous ...
Definition: AdamsMoulton.hpp:527
Y last_f()
Returns most recent f value. Can also access f array directly.
Definition: AdamsMoulton.hpp:496
void solve_initial_K(T t0, Y f0, Y g0)
Automatically sets the first K values for F (and dF), given a single initial value for F,...
Definition: AdamsMoulton.hpp:581
Y dgdt(Y ft, Y gt, T tt) const
Returns derivative, dg/dt(t), given f(t),g(t),t.
Definition: AdamsMoulton.hpp:509
Y dfdt(Y ft, Y gt, T tt) const
Returns derivative, df/dt(t), given f(t),g(t),t.
Definition: AdamsMoulton.hpp:505
void drive()
Overload of drive(T t) for 'default' case, where next t is defined as last_t + dt (for arithmetic/com...
Definition: AdamsMoulton.hpp:568
ODESolver2D(Y dt, const DerivativeMatrix< T, Y > *D)
Construct the solver. dt is the (constant) step size.
Definition: AdamsMoulton.hpp:487
std::array< Y, K > df
Arrays to store the previous K values of derivatives, df and dg.
Definition: AdamsMoulton.hpp:470
std::array< Y, K > f
Arrays to store the previous K values of f and g.
Definition: AdamsMoulton.hpp:467
Y dt()
Returns the step size.
Definition: AdamsMoulton.hpp:502
Contains classes and functions which use general N-step Adams Moulton method to solve systems of 2x2 ...
Definition: AdamsMoulton.hpp:11
constexpr bool is_complex_v
User-defined type-trait: Checks whether T is a std::complex type.
Definition: AdamsMoulton.hpp:109
constexpr T inner_product(const std::array< T, N > &a, const std::array< U, M > &b)
Inner product of two std::arrays.
Definition: AdamsMoulton.hpp:122
constexpr double c
speed of light in a.u. (=1/alpha)
Definition: PhysConst_constants.hpp:17
Holds the K+1 Adams-Moulton ak coefficients for the K-step AM method. Final one, aK,...
Definition: AdamsMoulton.hpp:244
static constexpr std::array< double, K > ak
First K coefficients: ak for k={0,1,...,K-1}.
Definition: AdamsMoulton.hpp:268
static constexpr double aK
Final aK coefficients: ak for k=K.
Definition: AdamsMoulton.hpp:270
Pure-virtual struct, holds the derivative matrix for 2x2 system of ODEs. Derive from this,...
Definition: AdamsMoulton.hpp:79
virtual Y a(T t) const =0
a,b,c,d are derivative matrix functions; all must be user implemented
virtual Y Sf(T) const
Sf and Sg are optional inhomogenous terms.
Definition: AdamsMoulton.hpp:86