integrators.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  1. # integrators.py: range of classes implementing integrators
  2. # Copyright 2022 Romain Serra
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from typing import Optional, List, Iterable, Tuple, Callable
  15. from abc import ABCMeta, abstractmethod
  16. import numpy as np
  17. from swiftt.taylor.complex_multivar_taylor import ComplexMultivarTaylor
  18. class Integrator(metaclass=ABCMeta):
  19. """Abstract class for the implementation of numerical integrators.
  20. Attributes:
  21. _func (Callable): function of the independent variable and the state vector defining the derivative of the
  22. latter w.r.t. the former.
  23. _order (int): order of integration scheme.
  24. """
  25. def __init__(self, func: Callable, order: int) -> None:
  26. """Constructor for class Integrator.
  27. Args:
  28. func (Callable):
  29. function of the independent variable and the state vector defining the derivative of the latter
  30. w.r.t. the former.
  31. order (int): order of integration scheme.
  32. """
  33. self._func = func
  34. self._order = order
  35. @abstractmethod
  36. def integrate(self, t0: float, tf: float, x0: np.ndarray, n_step: int, keep_history: bool) -> Tuple:
  37. """Abstract method to implement. Performs the numerical integration between initial to final values of
  38. independent variable, with provided initial conditions.
  39. Args:
  40. t0 (float): initial time.
  41. tf (float): final time.
  42. x0 (numpy.ndarray): initial conditions.
  43. n_step (int): number of integrations steps.
  44. keep_history (bool): set to True to return the whole history of successful steps, False to return
  45. only the initial and final states.
  46. """
  47. raise NotImplementedError
  48. class FixedstepIntegrator(Integrator, metaclass=ABCMeta):
  49. """Abstract class for the implementation of numerical integrators with a fixed step-size.
  50. """
  51. @staticmethod
  52. def step_size(t0: float, tf: float, n_step: int) -> float:
  53. """Static method computing the constant step-size corresponding to initial and final values of independent
  54. variable as well as number of integration steps.
  55. Args:
  56. t0 (float): initial value of independent variable.
  57. tf (float): final time.
  58. n_step (int): number of steps.
  59. Returns:
  60. float: step-size.
  61. """
  62. return (tf - t0) / n_step
  63. @abstractmethod
  64. def integration_step(self, t: float, x: np.ndarray, h):
  65. """Abstract method to be overwritten in classes inheriting from abstract class. Performs a single integration
  66. step.
  67. Args:
  68. t (float): current value of independent variable.
  69. x (numpy.ndarray): state vector at t.
  70. h (Union[float, ComplexMultivarTaylor]): step-size.
  71. """
  72. raise NotImplementedError
  73. def _precompute(self, h) -> None:
  74. """Function to overwrite in order to perform pre-computations, if any, ahead of integration.
  75. Args:
  76. h (Union[float, ComplexMultivarTaylor]): step-size.
  77. """
  78. return
  79. def integrate(self, t0: float, tf: float, x0: np.ndarray, n_step: int, keep_history: bool) -> Tuple[List, List]:
  80. """Function that performs integration between two values of independent variable. It is vectorized w.r.t. x0 if
  81. self._func is: in other words, several initial states can be propagated in one call (with the same value for the
  82. initial independent variable and the same number of steps).
  83. Args:
  84. t0 (float): initial value of independent variable.
  85. tf (float): final time.
  86. x0 (numpy.ndarray): initial conditions.
  87. n_step (int): number of integration steps to be performed.
  88. keep_history (bool): set to True to return the whole history of successful steps, False to return
  89. only the initial and final states.
  90. Returns:
  91. List[numpy.ndarray]: state vectors at integration steps of interest.
  92. List[float]: values taken by the independent variable at integration steps.
  93. """
  94. h = self.step_size(t0, tf, n_step)
  95. Ts, Xs = [t0], [x0]
  96. self._precompute(h)
  97. if keep_history:
  98. for k in range(0, n_step):
  99. Xs.append(self.integration_step(Ts[k], Xs[k], h))
  100. Ts.append(Ts[k] + h)
  101. else:
  102. # first step
  103. Xs.append(self.integration_step(t0, x0, h))
  104. Ts.append(t0 + h)
  105. # rest of integration
  106. for __ in range(1, n_step):
  107. Xs[1] = self.integration_step(Ts[1], Xs[1], h)
  108. Ts[1] += h
  109. return Xs, Ts
  110. class Euler(FixedstepIntegrator):
  111. """Class implementing the classic Euler integration scheme.
  112. """
  113. def __init__(self, func: Callable) -> None:
  114. """Constructor for Euler class.
  115. Args:
  116. func (Callable): function of the independent variable and the state vector defining the derivative of the
  117. latter w.r.t. the former.
  118. """
  119. FixedstepIntegrator.__init__(self, func, order=1)
  120. def integration_step(self, t: float, x, h):
  121. """Function performing a single integration step i.e. given the state vector at the current value t of
  122. the independent variable, approximates its value at t + h where h is the step-size.
  123. Args:
  124. t (float): current value of independent variable.
  125. x (numpy.ndarray): state vector at t.
  126. h (Union[float, ComplexMultivarTaylor]): step-size.
  127. Returns:
  128. numpy.ndarray: state vector at t + h.
  129. """
  130. return x + h * self._func(t, x)
  131. class Heun(FixedstepIntegrator):
  132. """Class implementing the Heun integration scheme.
  133. Attributes:
  134. _half_step (Union[float, ComplexMultivarTaylor]): stored half step-size.
  135. """
  136. def __init__(self, func: Callable) -> None:
  137. """Constructor for Heun class.
  138. Args:
  139. func (Callable): function of the independent variable and the state vector defining the derivative of the
  140. latter w.r.t. the former.
  141. """
  142. FixedstepIntegrator.__init__(self, func, order=2)
  143. self._half_step = None
  144. def _precompute(self, h) -> None:
  145. """Overload parent implementation in order to pre-compute the half step-size.
  146. Args:
  147. h (Union[float, ComplexMultivarTaylor]): step-size.
  148. """
  149. self._half_step = 0.5 * h
  150. def integration_step(self, t: float, x, h):
  151. """Function performing a single integration step i.e. given the state vector at the current value t of
  152. the independent variable, approximates its value at t + h where h is the step-size.
  153. Args:
  154. t (float): current value of independent variable.
  155. x (numpy.ndarray): state vector at t.
  156. h (Union[float, ComplexMultivarTaylor]): step-size.
  157. Returns:
  158. numpy.ndarray: state vector at t + h.
  159. """
  160. f1 = self._func(t, x) # function call
  161. x1 = x + h * f1
  162. f2 = self._func(t + h, x1) # function call
  163. return x + self._half_step * (f1 + f2)
  164. class RK4(FixedstepIntegrator):
  165. """Class implementing the classic Runge-Kutta 4 integration scheme.
  166. Attributes:
  167. _half_step (Union[float, ComplexMultivarTaylor]): stored half step-size.
  168. _one_third_step (Union[float, ComplexMultivarTaylor]): stored one third of step-size.
  169. _one_sixth_step (Union[float, ComplexMultivarTaylor]): stored one sixth of step-size.
  170. """
  171. def __init__(self, func: Callable) -> None:
  172. """Constructor for RK4 class.
  173. Args:
  174. func (Callable): function of the independent variable and the state vector defining the derivative of the
  175. latter w.r.t. the former.
  176. """
  177. FixedstepIntegrator.__init__(self, func, order=4)
  178. self._half_step = None
  179. self._one_third_step = None
  180. self._one_sixth_step = None
  181. def _precompute(self, h) -> None:
  182. """Overload parent implementation in order to pre-compute quantities such as the half step-size.
  183. Args:
  184. h (Union[float, ComplexMultivarTaylor]): step-size.
  185. """
  186. self._half_step = h / 2.
  187. self._one_third_step = h / 3.
  188. self._one_sixth_step = h / 6.
  189. def integration_step(self, t: float, x, h):
  190. """Function performing a single integration step i.e. given the state vector at the current value t of
  191. the independent variable, approximates its value at t + h where h is the step-size.
  192. Args:
  193. t (float): current value of independent variable.
  194. x (numpy.ndarray): state vector at t.
  195. h (Union[float, ComplexMultivarTaylor]): step-size.
  196. Returns:
  197. numpy.ndarray: state vector at t + h.
  198. """
  199. middle_time = t + self._half_step
  200. f1 = self._func(t, x) # function call
  201. x1 = x + self._half_step * f1
  202. f2 = self._func(middle_time, x1) # function call
  203. x2 = x + self._half_step * f2
  204. f3 = self._func(middle_time, x2) # function call
  205. x3 = x + h * f3
  206. f4 = self._func(t + h, x3) # function call
  207. return x + (self._one_sixth_step * (f1 + f4) + self._one_third_step * (f2 + f3))
  208. class BS(FixedstepIntegrator):
  209. """Class implementing the Bulirsch-Stoer integration scheme.
  210. Attributes:
  211. _sequence (numpy.ndarray): Bulirsch sequence of integers to be used in scheme.
  212. """
  213. def __init__(self, func: Callable, order: int) -> None:
  214. """Constructor for BS class.
  215. Args:
  216. func (Callable): function of the independent variable and the state vector defining the derivative of the
  217. latter w.r.t. the former.
  218. order (int): order of integrator.
  219. """
  220. FixedstepIntegrator.__init__(self, func, order)
  221. self._sequence = np.zeros(self._order, dtype=int)
  222. self._sequence[0] = 2
  223. if self._order > 1:
  224. self._sequence[1] = 4
  225. if self._order > 2:
  226. self._sequence[2] = 6
  227. for k in range(3, self._order):
  228. self._sequence[k] = 2 * self._sequence[k - 2]
  229. # pre-compute intermediate quantities for extrapolation
  230. self._aux_extrap = np.zeros((self._order + 1, self._order + 1))
  231. inter = 1. / np.flip(self._sequence)
  232. for i, el in enumerate(self._sequence[1:], 1):
  233. self._aux_extrap[i + 1, : i] = el * inter[-i:]
  234. self._aux_extrap = 1. / (self._aux_extrap ** 2 - 1.)
  235. def integration_step(self, t: float, x: np.ndarray, H) -> np.ndarray:
  236. """Function performing a single integration step i.e. given the state vector at the current value t of
  237. the independent variable, approximates its value at t + H where H is the step-size.
  238. Args:
  239. t (float): current value of independent variable.
  240. x (numpy.ndarray): state vector at t.
  241. H (Union[float, ComplexMultivarTaylor]): step-size.
  242. Returns:
  243. numpy.ndarray: state vector at t + H.
  244. """
  245. M = self._extrapolation(self._order, H, x, t)
  246. return M[-1]
  247. def _midpoint(self, n: int, H, y: np.ndarray, t: float) -> np.ndarray:
  248. """Function applying the mid-point rule of the Bulirsch-Stoer method.
  249. Args:
  250. n (int): order.
  251. H (Union[float, ComplexMultivarTaylor]): step-size.
  252. y (numpy.ndarray): current state vector.
  253. t (float): current value of independent variable.
  254. Returns:
  255. numpy.ndarray: output of mid-point rule.
  256. """
  257. h = H / float(n)
  258. h2 = 2. * h
  259. u0 = None
  260. f1 = self._func(t, y) # function call
  261. u1 = y + h * f1
  262. f2 = self._func(t + h, u1) # function call
  263. u2 = y + h2 * f2
  264. for j in range(2, n + 1):
  265. f = self._func(t + j * h, u2) # function call
  266. u2, u1, u0 = u1 + h2 * f, u2, u1
  267. return 0.25 * (u0 + u2) + 0.5 * u1
  268. def _extrapolation(self, i: int, H, y: np.ndarray, t: float) -> List[np.ndarray]:
  269. """Function performing the extrapolation according to the Bulirsch-Stoer algorithm.
  270. Args:
  271. i (int): extrapolation order.
  272. H (Union[float, ComplexMultivarTaylor]): step-size.
  273. y (numpy.ndarray): current state vector.
  274. t (float): current value of independent variable.
  275. Returns:
  276. List[numpy.ndarray]: concatenated extrapolated vectors.
  277. """
  278. M = [self._midpoint(self._sequence[i - 1], H, y, t)]
  279. if i > 1:
  280. Mp = self._extrapolation(i - 1, H, y, t) # recursive call
  281. for j, el in enumerate(Mp):
  282. eta = M[j]
  283. M.append(eta + (eta - el) * self._aux_extrap[i, j])
  284. return M
  285. class MultistepIntegrator(FixedstepIntegrator, metaclass=ABCMeta):
  286. """Abstract class for the implementation of multi-step integrators with fixed step-size.
  287. Attributes:
  288. saved_steps (List[numpy.ndarray]): values of state derivative at previous steps.
  289. _stepsize (Union[float, ComplexMultivarTaylor]): step-size.
  290. _beta (numpy.ndarray): vector of numbers used in integration scheme.
  291. _initializer (FixedstepIntegrator): integrator used to initialize the multi-step method.
  292. """
  293. def __init__(self, func: Callable, order: int) -> None:
  294. """Constructor for class MultistepIntegrator.
  295. Args:
  296. func (Callable): function of the independent variable and the state vector defining the derivative of the
  297. latter w.r.t. the former.
  298. order (int): order of integrator.
  299. """
  300. FixedstepIntegrator.__init__(self, func, order)
  301. self._stepsize = 0.
  302. self.saved_steps = []
  303. self._beta = None
  304. self._initializer: FixedstepIntegrator = None
  305. def update_saved_steps(self, t: float, x: np.ndarray) -> None:
  306. """Function updating the saved values of self._func at the past self._order steps.
  307. Args:
  308. t (float): current value of independent variable.
  309. x (numpy.ndarray): state vector at t.
  310. """
  311. self.saved_steps = self.saved_steps[1:] # shift
  312. f = self._func(t, x) # function call
  313. self.saved_steps.append(f)
  314. def update_state(self, x: np.ndarray) -> np.ndarray:
  315. """Function propagating the state vector over one integration step.
  316. Args:
  317. x (numpy.ndarray): current state vector.
  318. Returns:
  319. numpy.ndarray: state vector at next integration step.
  320. """
  321. dx = sum(step * beta for step, beta in zip(self.saved_steps, self._beta))
  322. return x + self._stepsize * dx
  323. def integration_step(self, t: float, x: np.ndarray, h=None) -> np.ndarray:
  324. """Function performing a single integration step.
  325. Args:
  326. t (float): current value of independent variable.
  327. x (numpy.ndarray): state vector at t.
  328. h (Union[float, ComplexMultivarTaylor]): step-size (dummy variable in multi-step integrator here to
  329. match parent signature)
  330. Returns:
  331. numpy.ndarray: state vector at t + self._stepsize.
  332. """
  333. xf = self.update_state(x)
  334. self.update_saved_steps(t + self._stepsize, xf)
  335. return xf
  336. def initialize(self, t0: float, x0: np.ndarray, h) -> Tuple[List, List]:
  337. """Function initializing the integrator with a single-step scheme.
  338. Args:
  339. t0 (float): initial value of independent variable.
  340. x0 (numpy.ndarray): state vector at t0.
  341. h (Union[float, ComplexMultivarTaylor]): step-size
  342. Returns:
  343. List[numpy.ndarray]: history of state vector after initialization with single-step integrator.
  344. List[float]: values of independent variable corresponding to history of state vector.
  345. """
  346. self._stepsize = h
  347. n_steps = self._order - 1
  348. states, ind_vars = self._initializer.integrate(t0, t0 + float(n_steps) * h, x0, n_steps, keep_history=True)
  349. self.saved_steps = [self._func(ind_var, state) for ind_var, state in zip(ind_vars, states)]
  350. return states, ind_vars
  351. def integrate(self, t0: float, tf: float, x0: np.ndarray, n_step: int, keep_history: bool,
  352. saved_steps: Optional[List] = None) -> Tuple[List, List]:
  353. """Function that performs integration between two values of independent variable. It is vectorized w.r.t. x0 if
  354. self._func is: in other words, several initial states can be propagated in one call (with the same value for the
  355. initial independent variable and the same number of steps).
  356. Args:
  357. t0 (float): initial value of independent variable.
  358. tf (float): final value of independent variable.
  359. x0 (numpy.ndarray): state vector at t0.
  360. n_step (int): number of integration steps to be performed.
  361. keep_history (bool): set to True to return the whole history of successful steps, False to return
  362. only the initial and final states.
  363. saved_steps (List[numpy.ndarray]): past values of self._func.
  364. Returns:
  365. List[numpy.ndarray]: state vectors at integration steps of interest.
  366. List[float]: values taken by the independent variable at integration steps.
  367. """
  368. self.saved_steps = []
  369. if saved_steps is not None and len(saved_steps) == self._order:
  370. # input saved steps are recyclable
  371. self.saved_steps = list(saved_steps)
  372. h = self.step_size(t0, tf, n_step)
  373. # initialize steps
  374. if self._stepsize != h or self.saved_steps == []:
  375. Xs, Ts = self.initialize(t0, x0, h)
  376. n_start = len(Ts) - 1 # number of steps already performed
  377. if n_step <= n_start:
  378. # enough steps have already been performed, integration is over
  379. if keep_history:
  380. return Xs[:n_step + 1], Ts[:n_step + 1]
  381. # keep only initial state and last step
  382. return [x0, Xs[n_step + 1]], [t0, Ts[n_step + 1]]
  383. if not keep_history:
  384. Ts, Xs = [t0, Ts[-1]], [x0, Xs[-1]]
  385. else:
  386. # step-size has not changed and there are available saved steps
  387. Ts, Xs = [t0, t0 + self._stepsize], [x0, self.integration_step(t0, x0)]
  388. n_start = 1 # number of steps already performed
  389. # perform the rest of the integration
  390. if keep_history:
  391. for k in range(n_start, n_step):
  392. Xs.append(self.integration_step(Ts[k], Xs[k]))
  393. Ts.append(Ts[k] + self._stepsize)
  394. else:
  395. for __ in range(n_start, n_step):
  396. Xs[1] = self.integration_step(Ts[1], Xs[1], self._stepsize)
  397. Ts[1] += self._stepsize
  398. return Xs, Ts
  399. class AB8(MultistepIntegrator):
  400. """Class implementing the Adam-Bashforth integration scheme of order 8.
  401. """
  402. def __init__(self, func: Callable) -> None:
  403. """Constructor for class AB8.
  404. Args:
  405. func (Callable): function of the independent variable and the state vector defining the derivative of the
  406. latter w.r.t. the former.
  407. """
  408. MultistepIntegrator.__init__(self, func, order=8)
  409. self._beta = np.array([-36799., 295767., -1041723., 2102243., -2664477., 2183877., -1152169., 434241.]) / 120960.
  410. self._initializer = BS(self._func, (self._order + 1) // 2)
  411. class ABM8(MultistepIntegrator):
  412. """Class implementing the Adam-Bashforth-Moulton integration scheme of order 8.
  413. """
  414. def __init__(self, func: Callable) -> None:
  415. """Constructor for class ABM8.
  416. Args:
  417. func (Callable): function of the independent variable and the state vector defining the derivative of the
  418. latter w.r.t. the former.
  419. """
  420. MultistepIntegrator.__init__(self, func, order=8)
  421. self._beta = np.array([1375., -11351., 41499., -88547., 123133., -121797., 139849., 36799.]) / 120960.
  422. self._predictor = self._initializer = AB8(self._func)
  423. def integration_step(self, t: float, x: np.ndarray, h=None) -> np.ndarray:
  424. """Function performing a single integration step.
  425. Args:
  426. t (float): current value of independent variable.
  427. x (numpy.ndarray): state vector at t.
  428. h (Union[float, ComplexMultivarTaylor]): step-size (dummy variable in multi-step integrator here to
  429. match parent signature)
  430. Returns:
  431. numpy.ndarray: state vector at t + self._stepsize.
  432. """
  433. self._predictor.integration_step(t, x) # (hides a function call)
  434. self.saved_steps = list(self._predictor.saved_steps)
  435. xf = self.update_state(x)
  436. f = self._func(t + self._stepsize, xf) # function call
  437. del self._predictor.saved_steps[-1]
  438. self._predictor.saved_steps.append(f)
  439. return xf
  440. class VariableStepIntegrator(Integrator, metaclass=ABCMeta):
  441. """Abstract class for the implementation of integrators with step-size control.
  442. Attributes:
  443. _dim_state (int): dimension of state vector.
  444. _last_step_ok (bool): false if last step didn't satisfy the constraint on the absolute error, true
  445. otherwise.
  446. _error_exponent (float): exponent used in derivation of estimate for integration error.
  447. _abs_tol (Iterable[float]): tolerance vector on estimated absolute error. Should have same number of
  448. components than there are state variables. Default is 1.e-8 for each.
  449. _rel_tol (Iterable[float]): tolerance vector on estimated relative error. Should have same number of
  450. components than there are state variables. Default is 1.e-4 for each.
  451. _max_stepsize (float): maximum step-size allowed. Default is + infinity.
  452. _step_multiplier (float): multiplicative factor to increase step-size when an integration step has
  453. been successful.
  454. """
  455. def __init__(self, func: Callable, order: int, dim_state: int, abs_error_tol=None, rel_error_tol=None,
  456. max_stepsize: Optional[float] = None, step_multiplier: Optional[float] = None,
  457. event_func: Optional[Callable] = None, tol_event: Optional[float] = None) -> None:
  458. """Constructor for class VariableStepIntegrator.
  459. Args:
  460. func (Callable): function of the independent variable and the state vector defining the derivative of the
  461. latter w.r.t. the former.
  462. order (int): order of integrator.
  463. dim_state (int): dimension of state factor.
  464. abs_error_tol (): tolerance vector on estimated absolute error. Should have same
  465. number of components than there are state variables. Default is 1.e-8 for each.
  466. rel_error_tol (): tolerance vector on estimated relative error. Should have same
  467. number of components than there are state variables. Default is 1.e-4 for each.
  468. max_stepsize (float): maximum step-size allowed. Default is + infinity.
  469. step_multiplier (float): multiplicative factor to increase step-size when an integration step has
  470. been successful.
  471. """
  472. Integrator.__init__(self, func, order)
  473. self._dim_state = dim_state
  474. self._last_step_ok = True
  475. self._error_exponent: float = None
  476. if event_func is None:
  477. self._event_func = None
  478. else:
  479. self._event_func = event_func
  480. self._tol_event = tol_event
  481. if tol_event is None:
  482. raise ValueError("An event-detection tolerance must be provided if an event function is given.")
  483. default_step_multiplier = 2.
  484. if step_multiplier is None:
  485. self._step_multiplier = default_step_multiplier
  486. else:
  487. if 1. <= step_multiplier <= 5.:
  488. self._step_multiplier = float(step_multiplier)
  489. else:
  490. print("input step multiplier is not in [1, 5], switching to default value of "
  491. + str(default_step_multiplier))
  492. self._step_multiplier = default_step_multiplier
  493. self._max_stepsize = np.inf if max_stepsize is None else max_stepsize
  494. default_abs_tol = 1.e-8
  495. self._abs_tol = np.full(self._dim_state, default_abs_tol)
  496. if abs_error_tol is not None:
  497. if len(abs_error_tol) != self._dim_state:
  498. raise ValueError("Wrong input in VariableStepIntegrator: tolerance on absolute error must have same "
  499. "dimension than state vector")
  500. for i, tol in enumerate(abs_error_tol):
  501. if tol <= 0.:
  502. print("Input tolerance on absolute error is negative, switching to default value of "
  503. + str(default_abs_tol) + " with state variable " + str(i))
  504. else:
  505. self._abs_tol[i] = tol
  506. default_rel_tol = 1.e-4
  507. self._rel_tol = np.full(self._dim_state, default_rel_tol)
  508. if rel_error_tol is not None:
  509. if len(rel_error_tol) != self._dim_state:
  510. raise ValueError("Wrong input in VariableStepIntegrator: tolerance on relative error must have same "
  511. "dimension than state vector")
  512. for i, tol in enumerate(rel_error_tol):
  513. if tol <= 0.:
  514. print("input tolerance on relative error is negative, switching to default value of "
  515. + str(default_rel_tol) + " with state variable " + str(i))
  516. else:
  517. self._rel_tol[i] = tol
  518. @abstractmethod
  519. def integration_step(self, t: float, x: np.ndarray, h):
  520. """Abstract method to be overwritten in classes inheriting from abstract class. Performs a single integration
  521. step.
  522. Args:
  523. t (float): current value of independent variable.
  524. x (numpy.ndarray): state vector at t.
  525. h (Union[float, ComplexMultivarTaylor]): current step-size.
  526. """
  527. raise NotImplementedError
  528. def integrate(self, t0: float, tf: float, x0: np.ndarray, n_step: int, keep_history: bool) -> Tuple[List, List]:
  529. """Function that performs integration between two values of independent variable.
  530. Args:
  531. t0 (float): initial value of independent variable.
  532. tf (float): final value of independent variable.
  533. x0 (numpy.ndarray): state vector at t0.
  534. n_step (int): initial guess for number of integration steps.
  535. keep_history (bool): set to True to return the whole history of successful steps, False to return
  536. only the initial and final states.
  537. Returns:
  538. List[numpy.ndarray]: state vectors at integration steps of interest.
  539. List[float]: values taken by the independent variable at integration steps.
  540. """
  541. if len(x0) != self._dim_state:
  542. raise ValueError("Wrong input in integrate: state vector has different dimension than the one given when "
  543. "the integrator was instantiated")
  544. # call dedicated method if there is an event function
  545. if self._event_func is None:
  546. return self._integrate_without_events(t0, tf, x0, n_step, keep_history)
  547. return self._integrate_with_events(t0, tf, x0, n_step, keep_history)
  548. def _integrate_without_events(self, t0: float, tf: float, x0: np.ndarray, n_step: int,
  549. keep_history: bool) -> Tuple[List, List]:
  550. """Function that performs integration between two values of independent variable without event detection.
  551. Args:
  552. t0 (float): initial value of independent variable.
  553. tf (float): final value of independent variable.
  554. x0 (numpy.ndarray): state vector at t0.
  555. n_step (int): initial guess for number of integration steps.
  556. keep_history (bool): set to True to return the whole history of successful steps, False to return
  557. only the initial and final states.
  558. Returns:
  559. List[numpy.ndarray]: state vectors at integration steps of interest.
  560. List[float]: values taken by the independent variable at integration steps.
  561. """
  562. # initial guess for step-size
  563. h = FixedstepIntegrator.step_size(t0, tf, n_step)
  564. # save direction of integration
  565. forward = tf > t0
  566. if keep_history:
  567. Ts, Xs = [t0], [x0]
  568. else:
  569. Ts, Xs = [t0, t0], [x0, x0]
  570. t = t0
  571. abs_dt = abs(tf - t0)
  572. while abs(t - t0) < abs_dt:
  573. # check and possibly decrease step-size
  574. if abs(h) > self._max_stepsize:
  575. h = self._max_stepsize if forward else -self._max_stepsize
  576. if (t + h > tf and forward) or (t + h < tf and not forward):
  577. h = tf - t
  578. # compute candidate new state and associated integration error
  579. x, err = self.integration_step(t, Xs[-1], h)
  580. # check viability of integration step
  581. if isinstance(x[0], ComplexMultivarTaylor):
  582. tol = self._abs_tol + np.array([el.norm for el in x]) * self._rel_tol
  583. err_ratios = np.array([el.norm for el in err]) / tol
  584. else:
  585. tol = self._abs_tol + np.fabs(x) * self._rel_tol
  586. err_ratios = np.fabs(err) / tol
  587. max_err_ratio = np.max(err_ratios)
  588. self._last_step_ok = max_err_ratio < 1.
  589. if self._last_step_ok:
  590. factor = self._step_multiplier
  591. t += h
  592. if keep_history:
  593. Ts.append(t)
  594. Xs.append(x)
  595. else:
  596. Ts[1], Xs[1] = t, x
  597. else:
  598. # step was not successful
  599. factor = 0.9 * (1. / float(max_err_ratio)) ** self._error_exponent
  600. # step-size update
  601. h *= factor
  602. return Xs, Ts
  603. def _integrate_with_events(self, t0: float, tf: float, x0: np.ndarray, n_step: int,
  604. keep_history: bool) -> Tuple[List, List]:
  605. """Function that performs integration between two values of independent variable whilst taking event detection
  606. into account, so possibly stopping early.
  607. Args:
  608. t0 (float): initial value of independent variable.
  609. tf (float): final value of independent variable.
  610. x0 (numpy.ndarray): state vector at t0.
  611. n_step (int): initial guess for number of integration steps.
  612. keep_history (bool): set to True to return the whole history of successful steps, False to return
  613. only the initial and final states.
  614. Returns:
  615. List[numpy.ndarray]: state vectors at integration steps of interest.
  616. List[float]: values taken by the independent variable at integration steps.
  617. """
  618. # initial guess for step-size
  619. h = FixedstepIntegrator.step_size(t0, tf, n_step)
  620. # save direction of integration
  621. forward = tf > t0
  622. if keep_history:
  623. Ts, Xs = [t0], [x0]
  624. else:
  625. Ts, Xs = [t0, t0], [x0, x0]
  626. t = t0
  627. abs_dt = abs(tf - t0)
  628. while abs(t - t0) < abs_dt:
  629. # check and possibly decrease step-size
  630. if abs(h) > self._max_stepsize:
  631. h = self._max_stepsize if forward else -self._max_stepsize
  632. if (t + h > tf and forward) or (t + h < tf and not forward):
  633. h = tf - t
  634. # compute candidate new state and associated integration error
  635. x, err = self.integration_step(t, Xs[-1], h)
  636. # check viability of integration step
  637. if isinstance(x[0], ComplexMultivarTaylor):
  638. tol = self._abs_tol + np.array([el.norm for el in x]) * self._rel_tol
  639. err_ratios = np.array([el.norm for el in err]) / tol
  640. else:
  641. tol = self._abs_tol + np.fabs(x) * self._rel_tol
  642. err_ratios = np.fabs(err) / tol
  643. max_err_ratio = np.max(err_ratios)
  644. self._last_step_ok = max_err_ratio < 1.
  645. if self._last_step_ok:
  646. factor = self._step_multiplier
  647. if self._event_func(t, x):
  648. if abs(h) > self._tol_event:
  649. # detection tolerance is not satisfied so step-size is halved
  650. factor = 0.5
  651. else:
  652. # event has been detected satisfyingly, integration is stopped
  653. tf = t
  654. abs_dt = abs(t - t0)
  655. if keep_history:
  656. Ts.append(t)
  657. Xs.append(x)
  658. else:
  659. Ts[1], Xs[1] = t, x
  660. else:
  661. t += h
  662. if keep_history:
  663. Ts.append(t)
  664. Xs.append(x)
  665. else:
  666. Ts[1], Xs[1] = t, x
  667. else:
  668. # step was not successful
  669. factor = 0.9 * (1. / float(max_err_ratio)) ** self._error_exponent
  670. # step-size update
  671. h *= factor
  672. return Xs, Ts
  673. class RKF45(VariableStepIntegrator):
  674. """Class implementing the Runge-Kutta-Fehlberg 4(5) integration scheme.
  675. Attributes:
  676. _factor_t3 (Union[float, ComplexMultivarTaylor]): pre-computed factor involved in calculation of t3
  677. _factor_t4 (Union[float, ComplexMultivarTaylor]): pre-computed factor involved in calculation of t4
  678. _factor_x2 (Union[float, ComplexMultivarTaylor]): pre-computed factor involved in calculation of x2
  679. _factor_x3 (Union[float, ComplexMultivarTaylor]): pre-computed factor involved in calculation of x3
  680. _factor_x4_f1 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f1 to obtain x4
  681. _factor_x4_f3 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f3 to obtain x4
  682. _factor_x4_f4 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f4 to obtain x4
  683. _factor_x5_f1 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f1 to obtain x5
  684. _factor_x5_f3 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f3 to obtain x5
  685. _factor_x5_f4 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f4 to obtain x5
  686. _factor_x5_f5 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f5 to obtain x5
  687. _factor_xf_f1 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f1 to obtain xf
  688. _factor_xf_f3 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f3 to obtain xf
  689. _factor_xf_f4 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f4 to obtain xf
  690. _factor_xf_f5 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f5 to obtain xf
  691. _factor_err_f1 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f1 to obtain err
  692. _factor_err_f3 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f3 to obtain err
  693. _factor_err_f4 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f4 to obtain err
  694. _factor_err_f5 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f5 to obtain err
  695. _factor_err_f6 (Union[float, ComplexMultivarTaylor]): pre-computed factor multiplied by f6 to obtain err
  696. """
  697. def __init__(self, func: Callable, dim_state: int, abs_error_tol=None, rel_error_tol=None,
  698. max_stepsize: Optional[float] = None, step_multiplier: Optional[float] = None,
  699. event_func: Optional[Callable] = None, tol_event: Optional[float] = None) -> None:
  700. VariableStepIntegrator.__init__(self, func, order=4, dim_state=dim_state, abs_error_tol=abs_error_tol,
  701. rel_error_tol=rel_error_tol, max_stepsize=max_stepsize,
  702. step_multiplier=step_multiplier, event_func=event_func, tol_event=tol_event)
  703. self._error_exponent = 1. / (self._order + 1.)
  704. self._factor_t3 = 3. / 8.
  705. self._factor_t4 = 12. / 13.
  706. self._factor_x2 = 3. / 32.
  707. self._factor_x3 = 1. / 2197.
  708. self._factor_x4_f1 = 439. / 216.
  709. self._factor_x4_f3 = 3680. / 513.
  710. self._factor_x4_f4 = -845. / 4104.
  711. self._factor_x5_f1 = -8. / 27.
  712. self._factor_x5_f3 = -3544. / 2565.
  713. self._factor_x5_f4 = 1859. / 4104.
  714. self._factor_x5_f5 = -11. / 40.
  715. self._factor_xf_f1 = 25. / 216.
  716. self._factor_xf_f3 = 1408. / 2565.
  717. self._factor_xf_f4 = 2197. / 4104.
  718. self._factor_xf_f5 = -1. / 5.
  719. self._factor_err_f1 = 16. / 135.
  720. self._factor_err_f3 = 6656. / 12825.
  721. self._factor_err_f4 = 28561. / 56430.
  722. self._factor_err_f5 = -9. / 50.
  723. self._factor_err_f6 = 2. / 55.
  724. def integration_step(self, t: float, x: np.ndarray, h) -> Tuple[np.ndarray, np.ndarray]:
  725. """Method performing a single integration step (satisfying the error tolerance or not).
  726. Args:
  727. t (float): current value of independent variable.
  728. x (numpy.ndarray): state vector at t.
  729. h (Union[float, ComplexMultivarTaylor]): current step-size.
  730. Returns:
  731. numpy.ndarray: tentative state vector at t + h.
  732. numpy.ndarray: estimated error vector.
  733. """
  734. # values of independent variable where the model will be evaluated
  735. t1 = t
  736. dt2 = 0.25 * h
  737. t2 = t + dt2
  738. t3 = t + h * self._factor_t3
  739. t4 = t + h * self._factor_t4
  740. t5 = t + h
  741. t6 = t + 0.5 * h
  742. f1 = self._func(t1, x) # function call
  743. x1 = x + dt2 * f1
  744. f2 = self._func(t2, x1) # function call
  745. x2 = x + h * self._factor_x2 * (f1 + f2 * 3.)
  746. f3 = self._func(t3, x2) # function call
  747. x3 = x + h * self._factor_x3 * (f1 * 1932. + f2 * (-7200.) + f3 * 7296.)
  748. f4 = self._func(t4, x3) # function call
  749. x4 = x + h * (f1 * self._factor_x4_f1 + f2 * (-8.) + f3 * self._factor_x4_f3 + f4 * self._factor_x4_f4)
  750. f5 = self._func(t5, x4) # function call
  751. x5 = x + h * (f1 * self._factor_x5_f1 + f2 * 2. + f3 * self._factor_x5_f3 + f4 * self._factor_x5_f4
  752. + f5 * self._factor_x5_f5)
  753. f6 = self._func(t6, x5) # function call
  754. inter1 = f1 * self._factor_xf_f1 + f3 * self._factor_xf_f3 + f4 * self._factor_xf_f4 + f5 * self._factor_xf_f5
  755. xf = h * inter1
  756. inter2 = f1 * self._factor_err_f1 + f3 * self._factor_err_f3 + f4 * self._factor_err_f4 \
  757. + f5 * self._factor_err_f5 + f6 * self._factor_err_f6
  758. x_hat = h * inter2
  759. err = xf - x_hat
  760. xf += x
  761. return xf, err