stepsize.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. """
  2. Set of classes that allow the computation of a new delta.
  3. """
  4. from copy import deepcopy
  5. class StepSize:
  6. """
  7. Base class for all step size changers.
  8. Warning:
  9. This class should not be instantiated by the user.
  10. Args:
  11. delta_t (numeric): The (starting) delta of the simulation.
  12. """
  13. def __init__(self, delta_t):
  14. self.delta_t = delta_t
  15. def setDeltaT(self, new_delta_t):
  16. """
  17. Updates the delta.
  18. Args:
  19. new_delta_t (numeric): The new delta.
  20. """
  21. self.delta_t = new_delta_t
  22. def getNextStepSize(self, sim):
  23. """
  24. Obtains the next step size of the simulation, based on the current state.
  25. Args:
  26. sim (CBD.simulator.Simulator): The simulation to compute the step size of.
  27. """
  28. raise NotImplementedError()
  29. class Fixed(StepSize):
  30. """
  31. A fixed step size. This is the default in a simulation.
  32. Does **not** change the delta itself. External processes
  33. may alter the delta without any issues when this class
  34. is used.
  35. :Example:
  36. In the figure below, the data from `this CSV file <./_static/data.csv>`_
  37. is plotted as a blue line. Additionally, the red circles indicate the points when new data
  38. is computed.
  39. .. image:: _figures/stepsize/fixed.png
  40. :width: 500
  41. Args:
  42. delta_t (numeric): The (starting) delta of the simulation.
  43. """
  44. def getNextStepSize(self, sim):
  45. return self.delta_t
  46. class Euler2(StepSize):
  47. """
  48. Euler 2-Step algorithm for changing the step size.
  49. This algorithm looks at the simulation value at :code:`dt` time in the future and computes the
  50. interpolated value between this point and the current point at time :code:`dt / 2`. This
  51. interpolated value is now compared against the simulation result at time :code:`dt / 2`. Whenever
  52. the absolute error exceeds :code:`epsilon`, the step size is reduced. Otherwise, a new interpolated
  53. simulation value of :code:`dt * 2` is compared as before. When this second error is too large,
  54. :code:`dt` is the best delta for the current step. If not, :code:`dt` is increased.
  55. Whenever :code:`dt` is increased/decreased, the :func:`getNextStepSize` is called recursively to
  56. find the best :code:`dt` as fast as possible. Increasing happens by doubling :code:`dt`, whereas
  57. decreasing happens using the following formula:
  58. .. math::
  59. \delta t_{new} = \delta t_{old}\cdot\dfrac{0.9\cdot\epsilon}{error}
  60. Note:
  61. When there are multiple simulation outputs, the most pessimistic one (the highest error) will be
  62. used as a point of reference.
  63. :Example:
  64. In the figure below, the data from `this CSV file <./_static/data.csv>`_
  65. is plotted as a blue line. Additionally, the red circles indicate the points when new data
  66. is computed.
  67. .. image:: _figures/stepsize/euler2.png
  68. :width: 500
  69. Args:
  70. delta_t (numeric): The initial delta (:code:`dt`).
  71. epsilon (numeric): The maximal acceptable absolute error
  72. See Also:
  73. `Professor Joel Feldman's notes on variable step size algorithms. <http://www.math.ubc.ca/~feldman/math/vble.pdf>`_
  74. """
  75. def __init__(self, delta_t, epsilon):
  76. StepSize.__init__(self, delta_t)
  77. self.epsilon = epsilon
  78. def getNextStepSize(self, sim):
  79. sim.setStepSize(Fixed(self.delta_t))
  80. sim._update_clock()
  81. sim._do_single_step()
  82. A1 = deepcopy(sim.model.getSignals())
  83. sim._step_back()
  84. sim.setDeltaT(self.delta_t / 2)
  85. sim._update_clock()
  86. sim._do_single_step()
  87. A2 = deepcopy(sim.model.getSignals())
  88. sim._step_back()
  89. sim.setStepSize(self)
  90. sim.setDeltaT(self.delta_t)
  91. r_max = float('-inf')
  92. for port in A1.keys():
  93. r_max = max(r_max, abs(self.inp(A1, port) - A2[port][-1].value))
  94. if r_max > self.epsilon:
  95. self.delta_t *= .9 * self.epsilon / r_max
  96. return self.getNextStepSize(sim)
  97. # No error => can we go larger?
  98. sim.setStepSize(Fixed(self.delta_t * 2))
  99. sim._update_clock()
  100. sim._do_single_step()
  101. A3 = deepcopy(sim.model.getSignals())
  102. sim._step_back()
  103. sim.setStepSize(self)
  104. sim.setDeltaT(self.delta_t)
  105. r_max = float('-inf')
  106. for port in A1.keys():
  107. r_max = max(r_max, abs(self.inp(A3, port) - A1[port][-1].value))
  108. if r_max > self.epsilon:
  109. return self.delta_t
  110. self.delta_t *= 2
  111. return self.getNextStepSize(sim)
  112. @staticmethod
  113. def inp(A1, port):
  114. """
  115. Find the midpoint-interpolation value of two sequential values.
  116. Args:
  117. A1 (dict): Signal dictionary to look at.
  118. port (str): Name of the port to compare.
  119. """
  120. return A1[port][-2].value - (A1[port][-2].value - A1[port][-1].value) / 2
  121. from CBD.lib.io import Interpolation
  122. class Euler2Alt(StepSize):
  123. def __init__(self, delta_t, epsilon):
  124. StepSize.__init__(self, delta_t)
  125. self.epsilon = epsilon
  126. self.t = []
  127. def getNextStepSize(self, sim):
  128. S = deepcopy(sim.model.getSignals())
  129. self.t.append(sim.getTime())
  130. h = self.delta_t
  131. A1 = {}
  132. A2 = {}
  133. for p in S.keys():
  134. y = S[p]
  135. yk = y[-1]
  136. tk = self.t[-1]
  137. A1[p] = yk + h * self.f(y, tk)
  138. A2[p] = yk + h/2 * self.f(y, tk) + h/2 * self.f(y, tk + h/2)
  139. def f(self, y, t):
  140. if t > self.t[-1]:
  141. return y[-1]
  142. xx = 0
  143. for x in self.t:
  144. if x > t:
  145. break
  146. xx += 1
  147. return Interpolation.interpolate((self.t[xx], y[xx]), (self.t[xx+1], y[xx+1]), t, Interpolation.LINEAR)
  148. class RKF45(StepSize): pass
  149. class ButcherTableau:
  150. r"""
  151. Mnemonic device to store the Runge-Kutta matrix, weights and nodes in
  152. the computation of generic RK methods. The extended tableau also allows
  153. for the error computation for adaptive step sizes. The general form of
  154. a Butcher Tableau is shown below, where:
  155. * :math:`s` identifies the number of stages;
  156. * :math:`a_{ij}, 1 \leq j < i \leq s` represents a coefficient in the
  157. Runge-Kutta matrix;
  158. * :math:`b_i` and :math:`b^*_i` correspond to the weights of a higher
  159. and a lower order method, respectively; and
  160. * :math:`c_i` specifies the nodes.
  161. .. math::
  162. \begin{array}
  163. {c|ccccc}
  164. 0\\
  165. c_2 & a_{2,1}\\
  166. c_3 & a_{3,1} & a_{3,2} \\
  167. \vdots & \vdots & & \ddots\\
  168. c_s & a_{s,1} & a_{s,2} & \cdots & a_{s,s-1}\\
  169. \hline
  170. & b_1 & b_2 & \cdots & b_{s-1} & b_s\\
  171. & b^*_1 & b^*_2 & \cdots & b^*_{s-1} & b^*_s\\
  172. \end{array}
  173. Args:
  174. rows (iter): Sequence of tuples :math:`(c_i, [a_{i, j}\vert 1 \leq j < i])`.
  175. When :code:`None`, nothing will be added.
  176. weights (iter): Sequence of sequences of weights :math:`[b_{i}\vert i \leq s]`.
  177. When :code:`None`, no weights will be added.
  178. Note:
  179. Upon instantiation, the first (empty) row will be added automatically with a
  180. node of 0.
  181. """
  182. def __init__(self, rows=None, weights=None):
  183. self._matrix = []
  184. self._weights = []
  185. self._nodes = [0]
  186. if rows is not None:
  187. for node, mtx in rows:
  188. self.addRow(node, mtx)
  189. if weights is not None:
  190. for w in weights:
  191. self.addWeights(w)
  192. def addRow(self, node, elems):
  193. r"""
  194. Adds a :math:`c_i` and :math:`a_{i, j}` to the tableau.
  195. Args:
  196. node (numeric): The :math:`c_i`-value.
  197. elems (iter): :math:`a_{i, j}`, :math:`\forall j < i`; i.e. the
  198. sequence of matrix elements that correspond to the node.
  199. """
  200. if len(self._nodes) != len(elems):
  201. raise ValueError("Inconsistent matrix! Expected row with %d elements!" % len(self._nodes))
  202. self._nodes.append(node)
  203. self._matrix.append(elems)
  204. def addWeights(self, *weights):
  205. """
  206. Adds a row of weights to the bottom of the matrix.
  207. Args:
  208. *weights: A sequence of the weights. I.e. :math:`b_{i}`, where :math:`1 \leq i \leq s`.
  209. """
  210. if len(self._matrix) == 0 or (len(self._matrix[-1]) + 1) != len(weights):
  211. raise ValueError("Trying to set weights on incomplete matrix")
  212. if len(self._weights) == 2:
  213. raise ValueError("Maximal amount of weight rows (2) reached")
  214. if abs(sum(weights) - 1) > 1e-6:
  215. raise ValueError("Inconsistent Butcher Tableau for Runge-Kutta approximation. "
  216. "The sum of the weights must equal 1.")
  217. self._weights.append(weights)
  218. def getNodes(self):
  219. """
  220. Obtains the nodes, i.e. the :math:`c_i`-values.
  221. """
  222. return self._nodes
  223. def getWeights(self):
  224. """
  225. Obtains the weight lists, i.e. the :math:`b_i` and :math:`b^*_i`-values.
  226. """
  227. return self._weights
  228. def getA(self, i, j):
  229. """
  230. Obtains an element from the Runge-Kutta matrix.
  231. Args:
  232. i (int): The row (1-indexed).
  233. j (int): The column (1-indexed).
  234. """
  235. return self._matrix[i - 1][j - 1]
  236. @staticmethod
  237. def Heun():
  238. r"""
  239. Creates and returns the Butcher Tableau for Heun's method.
  240. The Tableau is as follows:
  241. .. math::
  242. \begin{array}
  243. {c|cc}
  244. 0\\
  245. 1 & 1\\
  246. \hline
  247. & 1/2 & 1/2
  248. \end{array}
  249. """
  250. tab = ButcherTableau()
  251. tab.addRow(1, [1])
  252. tab.addWeights(1/2, 1/2)
  253. return tab
  254. @staticmethod
  255. def HeunEuler():
  256. r"""
  257. Creates and returns the extended Butcher Tableau for Heun's method,
  258. combined with the Euler method.
  259. The Tableau is as follows:
  260. .. math::
  261. \begin{array}
  262. {c|cc}
  263. 0\\
  264. 1 & 1\\
  265. \hline
  266. & 1/2 & 1/2\\
  267. & 1 & 0
  268. \end{array}
  269. """
  270. tab = ButcherTableau.Heun()
  271. tab.addWeights(1, 0)
  272. return tab
  273. @staticmethod
  274. def Ralston():
  275. r"""
  276. Creates and returns the Butcher Tableau for Ralston's method for 2nd order
  277. accuracy. The Tableau is as follows:
  278. .. math::
  279. \begin{array}
  280. {c|cc}
  281. 0\\
  282. 2/3 & 2/3\\
  283. \hline
  284. & 1/4 & 3/4
  285. \end{array}
  286. """
  287. tab = ButcherTableau()
  288. tab.addRow(2/3, [2/3])
  289. tab.addWeights(1/4, 3/4)
  290. return tab
  291. @staticmethod
  292. def RalstonEuler():
  293. r"""
  294. Creates and returns the extended Butcher Tableau for Ralston's method,
  295. combined with the Euler method.
  296. The Tableau is as follows:
  297. .. math::
  298. \begin{array}
  299. {c|cc}
  300. 0\\
  301. 2/3 & 2/3\\
  302. \hline
  303. & 1/4 & 3/4\\
  304. & 1 & 0
  305. \end{array}
  306. """
  307. tab = ButcherTableau.Ralston()
  308. tab.addWeights(1, 0)
  309. return tab
  310. @staticmethod
  311. def Midpoint():
  312. r"""
  313. Creates and returns the Butcher Tableau for the midpoint method.
  314. The Tableau is as follows:
  315. .. math::
  316. \begin{array}
  317. {c|cc}
  318. 0\\
  319. 1/2 & 1/2\\
  320. \hline
  321. & 0 & 1
  322. \end{array}
  323. """
  324. tab = ButcherTableau()
  325. tab.addRow(1/2, [1/2])
  326. tab.addWeights(0, 1)
  327. return tab
  328. @staticmethod
  329. def MidpointEuler():
  330. r"""
  331. Creates and returns the extended Butcher Tableau for the midpoint method,
  332. combined with the Euler method.
  333. The Tableau is as follows:
  334. .. math::
  335. \begin{array}
  336. {c|cc}
  337. 0\\
  338. 1/2 & 1/2\\
  339. \hline
  340. & 0 & 1\\
  341. & 1 & 0
  342. \end{array}
  343. """
  344. tab = ButcherTableau.Midpoint()
  345. tab.addWeights(1, 0)
  346. return tab
  347. @staticmethod
  348. def RK4():
  349. r"""
  350. Creates and returns the Butcher Tableau for the default RK
  351. algorithm.
  352. The Tableau is as follows:
  353. .. math::
  354. \begin{array}
  355. {c|cc}
  356. 0\\
  357. 1/2 & 1/2\\
  358. 1/2 & 0 & 1/2\\
  359. 1 & 0 & 0 & 1\\
  360. \hline
  361. & 1/6 & 1/3 & 1/3 & 1/6
  362. \end{array}
  363. """
  364. tab = ButcherTableau()
  365. tab.addRow(1/2, [1/2])
  366. tab.addRow(1/2, [1/2, 1/2])
  367. tab.addRow( 1, [ 0, 0, 1])
  368. tab.addWeights(1/6, 1/3, 1/3, 1/6)
  369. return tab
  370. @staticmethod
  371. def RK4Alt():
  372. r"""
  373. Creates and returns the Butcher Tableau for an alternative RK
  374. algorithm. It is also called the 3/8-rule.
  375. The Tableau is as follows:
  376. .. math::
  377. \begin{array}
  378. {c|cc}
  379. 0\\
  380. 1/3 & 1/3\\
  381. 2/3 & -1/3 & 1\\
  382. 1 & 1 & -1 & 1\\
  383. \hline
  384. & 1/8 & 3/8 & 3/8 & 1/8
  385. \end{array}
  386. """
  387. tab = ButcherTableau()
  388. tab.addRow(1/3, [ 1/3])
  389. tab.addRow(2/3, [-1/3, 1])
  390. tab.addRow( 1, [ 1, -1, 1])
  391. tab.addWeights(1/8, 3/8, 3/8, 1/8)
  392. return tab
  393. @staticmethod
  394. def RKF45():
  395. r"""
  396. Creates and returns the extended Butcher Tableau for the
  397. Runge-Kutta-Fehlberg algorithm of 4th and 5th order.
  398. The Tableau is as follows:
  399. .. math::
  400. \begin{array}
  401. {c|cc}
  402. 0\\
  403. 1/4 & 1/4\\
  404. 3/8 & 3/32 & 9/32\\
  405. 12/13 & 1932/2197 & -7200/2197 & 7296/2197\\
  406. 1 & 439/216 & -8 & 3680/513 & -845/4104\\
  407. 1/2 & -8/27 & 2 & -3544/2565 & 1859/4104 & -11/40\\
  408. \hline
  409. & 16/135 & 0 & 6656/12825 & 28561/56430 & -9/50 & 2/55\\
  410. & 25/216 & 0 & 1408/2565 & 2197/4104 & -1/5 & 0
  411. \end{array}
  412. """
  413. tab = ButcherTableau()
  414. tab.addRow( 1/4, [ 1/4])
  415. tab.addRow( 3/8, [ 3/32, 9/32])
  416. tab.addRow(12/13, [1932/2197, -7200/2197, 7296/2197])
  417. tab.addRow( 1, [ 439/216, -8, 3680/513, -845/4104])
  418. tab.addRow( 1/2, [ -8/27, 2, -3544/2565, 1859/4104, -11/40])
  419. tab.addWeights( 16/135, 0, 6656/12825, 28561/56430, -9/50, 2/55)
  420. tab.addWeights( 25/216, 0, 1408/2565, 2197/4104, -1/5, 0)
  421. return tab
  422. @staticmethod
  423. def DOPRI():
  424. r"""
  425. Creates and returns the extended Butcher Tableau for the
  426. `Dormand-Prince method <https://www.sciencedirect.com/science/article/pii/0771050X80900133?via%3Dihub>`_.
  427. This is the default method in the :code:`ode45` solver for MATLAB and GNU Octave, among others.
  428. The Tableau is as follows:
  429. .. math::
  430. \begin{array}
  431. {c|cc}
  432. 0\\
  433. 1/5 & 1/5\\
  434. 3/10 & 3/40 & 9/40\\
  435. 4/5 & 44/45 & -56/15 & 32/9\\
  436. 8/9 & 19372/6561 & -25360/2187 & 64448/6561 & -212/729\\
  437. 1 & 9017/3168 & -355/33 & 46732/5247 & 49/176 & -5103/18656\\
  438. 1 & 35/384 & 0 & 500/1113 & 125/192 & -2187/6784 & 11/84\\
  439. \hline
  440. & 35/384 & 0 & 500/1113 & 125/192 & -2187/6784 & 11/84 & 0\\
  441. & 5179/57600 & 0 & 7571/16695 & 393/640 & -92097/339200 & 187/2100 & 1/40
  442. \end{array}
  443. """
  444. tab = ButcherTableau()
  445. tab.addRow( 1/5, [ 1/5])
  446. tab.addRow(3/10, [ 3/40, 9/40])
  447. tab.addRow( 4/5, [ 44/45, -56/15, 32/9])
  448. tab.addRow( 8/9, [ 19372/6561, -25360/2187, 64448/6561, -212/729])
  449. tab.addRow( 1, [ 9017/3168, -355/33, 46732/5247, 49/176, -5103/18656])
  450. tab.addRow( 1, [ 35/384, 0, 500/1113, 125/192, -2187/6784, 11/84])
  451. tab.addWeights( 35/384, 0, 500/1113, 125/192, -2187/6784, 11/84, 0)
  452. tab.addWeights( 5179/57600, 0, 7571/16695, 393/640, -92097/339200, 187/2100, 1/40)
  453. return tab
  454. RKDP = DOPRI
  455. "Alias of :func:`DOPRI`."
  456. DormandPrince = DOPRI
  457. "Alias of :func:`DOPRI`."
  458. @staticmethod
  459. def RKCK():
  460. r"""
  461. Creates and returns the extended Butcher Tableau for the
  462. `Cash-Karp method <https://dl.acm.org/doi/10.1145/79505.79507>`_ for 4th and 5th order
  463. accurate solutions.
  464. The Tableau is as follows:
  465. .. math::
  466. \begin{array}
  467. {c|cc}
  468. 0\\
  469. 1/5 & 1/5\\
  470. 3/10 & 3/40 & 9/40\\
  471. 3/5 & 3/10 & -9/10 & 6/5\\
  472. 1 & -11/54 & 5/2 & -70/27 & 35/27\\
  473. 7/8 & 1631/55296 & 175/512 & 575/13824 & 44275/110592 & 253/4096\\
  474. \hline
  475. & 37/378 & 0 & 250/621 & 125/594 & 0 & 512/1771\\
  476. & 2825/27648 & 0 & 18575/48384 & 13525/55296 & 277/14336 & 1/4
  477. \end{array}
  478. """
  479. tab = ButcherTableau()
  480. tab.addRow( 1/5, [ 1/5])
  481. tab.addRow(3/10, [ 3/40, 9/40])
  482. tab.addRow( 3/5, [ 3/10, -9/10, 6/5])
  483. tab.addRow( 1, [ -11/54, 5/2, -70/27, 35/27])
  484. tab.addRow( 7/8, [ 1631/55296, 175/512, 575/13824, 44275/110592, 253/4096])
  485. tab.addWeights( 37/378, 0, 250/621, 125/594, 0, 512/1771)
  486. tab.addWeights( 2825/27648, 0, 18575/48384, 13525/55296, 277/14336, 1/4)
  487. return tab
  488. CashKarp = RKCK
  489. "Alias of :func:`RKCK`."
  490. @staticmethod
  491. def BogackiShampine():
  492. r"""
  493. Creates and returns the extended Butcher Tableau for the
  494. `Bogacki-Shampine method <https://doi.org/10.1016%2F0893-9659%2889%2990079-7>`_ for 3th order
  495. accurate solutions.
  496. It is implemented in the :code:`ode23` function in MATLAB.
  497. The Tableau is as follows:
  498. .. math::
  499. \begin{array}
  500. {c|cc}
  501. 0\\
  502. 1/2 & 1/2\\
  503. 3/4 & 0 & 3/4\\
  504. 1 & 2/9 & 1/3 & 4/9\\
  505. \hline
  506. & 2/9 & 1/3 & 4/9 & 0\\
  507. & 7/24 & 1/4 & 1/3 & 1/8
  508. \end{array}
  509. """
  510. tab = ButcherTableau()
  511. tab.addRow(1/2, [ 1/2])
  512. tab.addRow(3/4, [ 0, 3/4])
  513. tab.addRow( 1, [ 2/9, 1/3, 4/9])
  514. tab.addWeights( 2/9, 1/3, 4/9, 0)
  515. tab.addWeights( 7/24, 1/4, 1/3, 1/8)
  516. return tab
  517. class RungeKutta(StepSize):
  518. r"""
  519. Applies the Runge-Kutta algorithm on a specific :class:`ButcherTableau`.
  520. Internally, a set of :math:`k_i` values will be computed, based on the
  521. tableau. From there, an error :math:`e` can be obtained. If this error
  522. exceeds :attr:`epsilon`, :math:`\delta t` is increased. The formulas
  523. are as follows:
  524. .. math::
  525. \begin{align}
  526. k_i &= f\left(t_n + h\cdot c_i, y_n + h\sum_{j=1}^{s-1}\left(a_{i,j}\cdot k_j\right)\right)\\
  527. e &= h\sum_{i=1}^s \left(b_i - b^*_i\right) k_i\\
  528. \delta t_{new} &= \delta t_{old}\cdot\dfrac{0.9\cdot\epsilon}{e}
  529. \end{align}
  530. Args:
  531. delta_t (numeric): Initial delta.
  532. tableau (ButcherTableau): The tableau to use.
  533. tol1 (numeric): Smallest allowed error value when decreasing delta.
  534. tol2 (numeric): Largest allowed error value when increasing delta.
  535. min_change (numeric): Minimal change for the delta to experience.
  536. max_change (numeric): Maximal change for the delta to experience.
  537. Note:
  538. If there are multiple outputs in a model, the most pessimistic view
  539. will be used, i.e. the largest error.
  540. See Also:
  541. https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods
  542. """
  543. def __init__(self, delta_t, tableau, tol1, tol2, min_change=0.3, max_change=2):
  544. StepSize.__init__(self, delta_t)
  545. self._tableau = tableau
  546. self._tol1 = tol1
  547. self._tol2 = tol2
  548. self._change = min_change, max_change
  549. self._prev = None
  550. def getNextStepSize(self, sim):
  551. A1 = sim.model.getSignals()
  552. K = []
  553. for n in self._tableau.getNodes():
  554. K.append(self.derivative(sim, n * self.delta_t))
  555. E = {}
  556. b, b_ = self._tableau.getWeights()
  557. s = len(b)
  558. for k in A1.keys():
  559. e = 0
  560. for i in range(s):
  561. e += K[i][k] * (b[i] - b_[i])
  562. E[k] = abs(e)
  563. TE = max(E.values())
  564. # TODO: going larger => HOW?
  565. # TODO: update formulas in docs
  566. if TE > self._tol1:
  567. self.delta_t *= 0.9 * min(max((self._tol1 / TE) ** (1 / (s - 1)), self._change[0]), self._change[1])
  568. # self.delta_t *= 0.9 * ((self._epsilon / TE) ** (1/(s-1)))
  569. if self._prev is None or abs(self._prev - self.delta_t) > 1e-6:
  570. self._prev = self.delta_t
  571. return self.getNextStepSize(sim)
  572. if TE < self._tol2:
  573. self.delta_t /= 0.9 * min(max((TE / self._tol2) ** (1 / (s - 1)), self._change[0]), self._change[1])
  574. # self.delta_t = self.delta_t * 2 + self._tol2
  575. return self.getNextStepSize(sim)
  576. self._prev = None
  577. return self.delta_t
  578. def derivative(self, sim, delta):
  579. """
  580. Computes the derivative; i.e. the :math:`f(t, y)`-function, which is the derivative definition
  581. for an initial-value problem. The derivative is computed as a backwards euler difference
  582. between :math:`t+\delta` and :math:`t`.
  583. Args:
  584. sim (CBD.simulator.Simulator): The simulation to compute the step size of.
  585. delta (numeric): The :math:`\delta` value of the next point.
  586. Note:
  587. Given that a CBD model represents the integrated version of the :math:`f(t, y)`-function,
  588. the :math:`y` parameter will be assumed to be set implicitly. It is henceforth ignored.
  589. """
  590. A1 = _copy_state(sim.model.getSignals())
  591. sim.setStepSize(Fixed(delta))
  592. sim._update_clock()
  593. sim._do_single_step()
  594. A2 = _copy_state(sim.model.getSignals())
  595. sim._step_back()
  596. sim.setStepSize(self)
  597. res = {}
  598. for key in A2.keys():
  599. if delta <= 1e-6:
  600. res[key] = 0
  601. else:
  602. res[key] = (A2[key][-1].value - A1[key][-1].value)
  603. return res
  604. def _copy_state(D):
  605. """
  606. Helper function that is an efficient version of deepcopy,
  607. by only getting the required data. The data is a dictionary
  608. of :code:`id -> list`, where the two last items of the list
  609. are of importance.
  610. Args:
  611. D (dict): A dictionary of lists.
  612. """
  613. return {k: v[-2:] for k, v in D.items()}
  614. from .simulator import Simulator