rungekutta.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. """
  2. This module contains all the logic for Runge-Kutta preprocessing.
  3. """
  4. from CBD.CBD import CBD
  5. from CBD.lib.std import *
  6. class RKPreprocessor:
  7. r"""
  8. Preprocesses a model to allow for Runge-Kutta approximation. This may be used to solve
  9. systems/initial-value problems in the form of
  10. .. math::
  11. \dfrac{dy}{dt} = f(t, y)
  12. Both normal approximation as well as adaptive stepsize can be done with this preprocessor.
  13. Args:
  14. tableau (CBD.preprocessing.butcher.ButcherTableau): The tableau for which RK approximation
  15. may be done. When this is a normal tableau, mere approximation will
  16. happen. When it is an extended tableau, the scale factor for the delta
  17. will also be computed.
  18. atol (float): The absolute tolerance for precision in approximating, given that the
  19. tableau is an extended tableau. Defaults to 1e-8.
  20. hmin (float): Minimal value for the delta, given that the tableau is an extended
  21. tableau. Defaults to 1e-40.
  22. hmax (float): Maximal value for the delta, given that the tableau is an extended
  23. tableau. Defaults to 1e-1.
  24. safety (float): Safety factor for the error computation. Must be in (0, 1], preferrably
  25. on the high end of the range. Defaults to 0.9.
  26. """
  27. def __init__(self, tableau, atol=1e-8, hmin=1e-40, hmax=1e-1, safety=0.9):
  28. self._tableau = tableau
  29. self._tolerance = atol
  30. self._h_range = hmin, hmax
  31. self._safety = safety
  32. def preprocess(self, original):
  33. """
  34. Do the actual preprocessing on a model.
  35. The model will be cloned and than flattened, such that the groups limited by
  36. :class:`CBD.lib.std.IntegratorBlock` and other memory blocks are collected
  37. as the initial-value problem they represent. From there, a new CBD model will
  38. be constructed, representative of the Runge-Kutta approximation with a given
  39. Butcher Tableau.
  40. When there are no :class:`CBD.lib.std.IntegratorBlock` available in the model,
  41. the original model will be returned.
  42. Args:
  43. original (CBD.CBD.CBD): A CBD model to get the RK approximating model for.
  44. Note:
  45. Currently, this fuction will yield undefined behaviour if the original model
  46. has input ports with a name that matches :code:`(IN\\d+|((rel_)?time))`,
  47. output ports that match :code:`OUT\\d+` and non-clock steering blocks with
  48. prefix :code:`"clock"`.
  49. """
  50. # TODO: rename colliding blocks (e.g. change the capitalization or add a prefix for RK-system)
  51. # 1. Detect IVP based on integrators
  52. IVP, plinks = self.create_IVP(original)
  53. if len(plinks) == 0:
  54. return original
  55. # 2. Create an RK-model, based on the given tableau
  56. RK = self.create_RK(IVP)
  57. # 3. Substitute the RK-model collection in the original CBD
  58. outputs = original.getSignals().keys()
  59. inputs = original.getInputPortNames()
  60. new_model = CBD(original.getBlockName(), inputs, outputs)
  61. new_model.addBlock(RK)
  62. for inp in inputs:
  63. new_model.addConnection(inp, RK, inp)
  64. old_clock = original.getClock()
  65. new_model.addBlock(Clock("clock", old_clock.getTime(0) - old_clock.getRelativeTime(0)))
  66. new_model.addConnection("clock", RK, 'time', 'time')
  67. new_model.addConnection("clock", RK, 'rel_time', 'rel_time')
  68. if RK.hasBlock("h_new"):
  69. # TODO: take delta from original
  70. new_model.addBlock(ConstantBlock("HIC", 0.1))
  71. new_model.addBlock(DelayBlock("HDelay"))
  72. new_model.addConnection(RK, "HDelay", output_port_name="h_new")
  73. new_model.addConnection("HIC", "HDelay", input_port_name="IC")
  74. new_model.addConnection("HDelay", "clock", input_port_name="h")
  75. new_model.addConnection("HDelay", RK, input_port_name="h")
  76. else:
  77. # TODO: take delta from original
  78. new_model.addBlock(ConstantBlock("clock-delta", 0.1))
  79. new_model.addConnection("clock-delta", "clock", "h")
  80. new_model.addConnection("clock-delta", RK, "h")
  81. for y in outputs:
  82. itg, mop = original.getBlockByName(y).getBlockConnectedToInput("IN1")
  83. if itg.getBlockName() in plinks:
  84. p = plinks[itg.getBlockName()]
  85. new_model.addConnection(RK, y, output_port_name='OUT%d' % p)
  86. ic, icop = itg.getBlockConnectedToInput("IC")
  87. collection = self.collect(ic, finish=[IntegratorBlock, Clock, TimeBlock])
  88. if not new_model.hasBlock(ic.getBlockName()):
  89. new_model.addBlock(ic.clone())
  90. for block in collection:
  91. if not new_model.hasBlock(block.getBlockName()):
  92. new_model.addBlock(block.clone())
  93. for block in collection:
  94. cs = block.getInputPortNames()
  95. for c in cs:
  96. cb, op = block.getBlockConnectedToInput(c)
  97. if cb.getBlockName() in plinks:
  98. raise ValueError("Too complex Initial Condition for integrator %s" % cb.getBlockName())
  99. elif cb.getBlockType() == "TimeBlock":
  100. new_model.addConnection("clock", block.getBlockName(), c, "time")
  101. elif cb.getBlockType() == "Clock":
  102. new_model.addConnection("clock", block.getBlockName(), c, op)
  103. else:
  104. new_model.addConnection(cb.getBlockName(), block.getBlockName(), c, op)
  105. new_model.addConnection(cb.getBlockName(), block.getBlockName(), c, op)
  106. new_model.addConnection(ic.getBlockName(), RK, "IC%d" % p, icop)
  107. else:
  108. # parallel process
  109. collection = self.collect(original.getBlockByName(y), finish=[IntegratorBlock, Clock, TimeBlock])
  110. for block in collection:
  111. if not new_model.hasBlock(block.getBlockName()):
  112. new_model.addBlock(block.clone())
  113. for block in collection:
  114. cs = block.getInputPortNames()
  115. for c in cs:
  116. cb, op = block.getBlockConnectedToInput(c)
  117. if cb.getBlockName() in plinks:
  118. new_model.addConnection(RK, block.getBlockName(), c, "OUT%d" % plinks[cb.getBlockName()])
  119. elif cb.getBlockType() == "TimeBlock":
  120. new_model.addConnection("clock", block.getBlockName(), c, "time")
  121. elif cb.getBlockType() == "Clock":
  122. new_model.addConnection("clock", block.getBlockName(), c, op)
  123. else:
  124. new_model.addConnection(cb.getBlockName(), block.getBlockName(), c, op)
  125. new_model.addConnection(itg.getBlockName(), y, output_port_name=mop)
  126. return new_model
  127. def collect(self, start, sport=None, finish=None):
  128. """
  129. Breadth-first search collection of all blocks, starting from the start block and
  130. ending when it can't anymore or when it must finish.
  131. Args:
  132. start (CBD.CBD.BaseBlock): The block to start from. This block will be excluded
  133. from the collection.
  134. sport (iter): The set of ports on the start block to use. When
  135. :code:`None` or omitted, all ports will be used.
  136. Note that only the start block can have a specification
  137. for the allowed ports.
  138. finish (iter): A set of block types (not strings, the actual types!) to
  139. exclude from the collection, halting a branch whenever
  140. one of these has been reached.
  141. """
  142. if finish is None:
  143. finish = []
  144. collection = [x[1].block for x in start.getLinksIn().items() if \
  145. ((sport is not None and x[0] in sport) or (sport is None)) \
  146. and not isinstance(x[1].block, tuple(finish))]
  147. n_collection = [x.getBlockName() for x in collection]
  148. for block in collection:
  149. ccoll = self.collect(block, None, finish)
  150. for child in ccoll:
  151. cname = child.getBlockName()
  152. if cname not in n_collection:
  153. n_collection.append(cname)
  154. collection.append(child)
  155. return collection
  156. def create_IVP(self, original):
  157. """
  158. Detects the set of equations that make up the initial-value problem and
  159. constructs a CBD submodel that contains them. Multiple equations, branches
  160. and extra inputs are all taken into account.
  161. For every integrator, the IVP will contain an input and an output port,
  162. who will be linked as such.
  163. Args:
  164. original: The model to create the IVP for. This model will **not** be
  165. altered by this fuction.
  166. Returns:
  167. Tuple of :code:`IVP, plinks` where :code:`IVP` identifies the CBD for the
  168. IVP equations and :code:`plinks` a dictionary of
  169. :code:`IntegratorBlock name -> index`.
  170. """
  171. model = original.clone()
  172. model.flatten(ignore=[IntegratorBlock, Clock])
  173. blocks = model.getBlocks()
  174. IVP = CBD("IVP", ["time", "rel_time"], [])
  175. i = 0
  176. plinks = {}
  177. iblocks = []
  178. for block in blocks:
  179. if isinstance(block, DelayBlock):
  180. raise RuntimeError("Impossible to construct Runge-Kutta model for Delay Differential Equations!")
  181. if isinstance(block, IntegratorBlock):
  182. # Identify all integrators and give them their in- and outputs
  183. i += 1
  184. IVP.addInputPort("IN%d" % i)
  185. IVP.addOutputPort("OUT%d" % i)
  186. # Links for future reference
  187. plinks[block.getBlockName()] = i
  188. iblocks.append(block)
  189. for block in iblocks:
  190. collection = self.collect(block, ["IN1"], [IntegratorBlock, TimeBlock, Clock])
  191. # First add the blocks
  192. for child in collection:
  193. if child.getBlockType() == "InputPortBlock":
  194. IVP.addInputPort(child.getBlockName())
  195. elif child.getBlockType() == "OutputPortBlock":
  196. IVP.addOutputPort(child.getBlockName())
  197. else:
  198. IVP.addBlock(child.clone())
  199. # Next, link the blocks
  200. for child in collection:
  201. for name_input, link in child.getLinksIn().items():
  202. lbn = link.block.getBlockName()
  203. lop = link.output_port
  204. if not IVP.hasBlock(lbn):
  205. # Both the TimeBlock and the Clock's time output will be explicitly inputted,
  206. # hence, it is the predefined port
  207. if link.block.getBlockType() == "TimeBlock":
  208. lbn = 'time'
  209. elif link.block.getBlockType() == "Clock":
  210. lbn = lop
  211. elif lbn in plinks:
  212. # Other integrator input
  213. p = plinks[lbn]
  214. lbn = "IN%d" % p
  215. else:
  216. # DelayBlock or Clock outputs that have not been linked yet
  217. lbn = name_input + "-" + child.getBlockName()
  218. IVP.addInputPort(lbn)
  219. lop = None
  220. IVP.addConnection(lbn, child.getBlockName(), name_input, lop)
  221. # Link the output
  222. p = plinks[block.getBlockName()]
  223. fin = block.getBlockConnectedToInput("IN1")
  224. IVP.addConnection(fin.block.getBlockName(), "OUT%d" % p, None, fin.output_port)
  225. return IVP, plinks
  226. def create_RK(self, f):
  227. """
  228. Creates the CBD for determining a Runge-Kutta weighed sum in the form of
  229. .. math::
  230. y_{n+1} = y_n + \sum_{i=1}^s b_i k_i
  231. The full CBD model (for a standard, non-extended butcher tableau) looks
  232. like this (where the :math:`K`-blocks are constructed with the :func:`create_K`
  233. function):
  234. .. image:: _figures/rungekutta.png
  235. :width: 600
  236. Args:
  237. f (CBD.CBD.CBD): The CBD representing the actual IVP for which the
  238. RK approximation must be done.
  239. """
  240. # TODO: optimizations:
  241. # - adder "YSum_2_?" is not required for higher order, since it is
  242. # always followed by a subtraction of the same y_n-value
  243. RK = CBD("RK", ["h", "time", "rel_time"])
  244. fy = range(1, len(f.getSignals()) + 1)
  245. weights = list(reversed(self._tableau.getWeights()))
  246. s = len(weights[0])
  247. for y in fy:
  248. RK.addInputPort("IC%d" % y)
  249. RK.addOutputPort("OUT%d" % y)
  250. RK.addBlock(DelayBlock("delay_%d" % y))
  251. RK.addConnection("IC%d" % y, "delay_%d" % y, input_port_name='IC')
  252. RK.addConnection("delay_%d" % y, "OUT%d" % y)
  253. inpnames = [x for x in f.getInputPortNames() if x not in ["time", "rel_time"] + ["IN%d" % x for x in fy]]
  254. for inp in inpnames:
  255. RK.addInputPort(inp)
  256. for q in range(len(weights)):
  257. for y in fy:
  258. RK.addBlock(AdderBlock("RKSum_%d_%d" % (q + 1, y), s))
  259. RK.addBlock(AdderBlock("YSum_%d_%d" % (q + 1, y)))
  260. RK.addConnection("RKSum_%d_%d" % (q + 1, y), "YSum_%d_%d" % (q + 1, y))
  261. if q == 0 or q != len(weights) - 1:
  262. RK.addConnection("YSum_%d_%d" % (q + 1, y), "delay_%d" % y, input_port_name='IN1')
  263. RK.addConnection("delay_%d" % y, "YSum_%d_%d" % (q + 1, y))
  264. for i in range(s):
  265. j = i + 1
  266. RK.addBlock(self.create_K(j, f.clone()))
  267. for inp in inpnames:
  268. RK.addConnection(inp, "RK-K_%d" % j, inp)
  269. for y in fy:
  270. RK.addConnection("delay_%d" % y, "RK-K_%d" % j, input_port_name='IN%d' % y)
  271. for p in range(len(weights)):
  272. q = p + 1
  273. RK.addBlock(ConstantBlock("B%d_%d" % (q, j), weights[p][i]))
  274. for y in fy:
  275. RK.addBlock(ProductBlock("Mult%d_%d_%d" % (q, y, j)))
  276. RK.addConnection("B%d_%d" % (q, j), "Mult%d_%d_%d" % (q, y, j))
  277. RK.addConnection("RK-K_%d" % j, "Mult%d_%d_%d" % (q, y, j), output_port_name="OUT%d" % y)
  278. RK.addConnection("Mult%d_%d_%d" % (q, y, j), "RKSum_%d_%d" % (q, y))
  279. RK.addConnection("h", "RK-K_%d" % j, "h")
  280. RK.addConnection("time", "RK-K_%d" % j, "time")
  281. RK.addConnection("rel_time", "RK-K_%d" % j, "rel_time")
  282. for s in range(i):
  283. RK.addConnection("RK-K_%d" % (s+1), "RK-K_%d" % j, "k_%d" % (s+1))
  284. # Error Computation
  285. if len(weights) == 2:
  286. RK.addOutputPort("h_new")
  287. RK.addBlock(self.create_Error(len(fy)))
  288. for y in fy:
  289. for q in range(len(weights)):
  290. RK.addConnection("YSum_1_%d" % y, "error", "y_%d" % y)
  291. RK.addConnection("YSum_2_%d" % y, "error", "z_%d" % y)
  292. RK.addConnection("h", "error", "h")
  293. RK.addConnection("error", "h_new", output_port_name='h_new')
  294. return RK
  295. def create_K(self, s, f):
  296. r"""
  297. Creates the CBD for determining the :math:`k_s`-value in the Runge-Kutta
  298. approximation computation. The generic formula is:
  299. .. math::
  300. k_s = h\cdot f(t_n + c_s\cdot h, y_n + \sum_{i=1}^{s-1}a_{s, i} k_i)
  301. Which gives the following CBD model:
  302. .. image:: _figures/rungekutta.png
  303. :width: 600
  304. Args:
  305. s (int): The :math:`s`-value of the :math:`k_s` to compute.
  306. f (CBD.CBD.CBD): The CBD representing the actual IVP for which the
  307. RK approximation must be done.
  308. """
  309. input_ports = ["h", "time", "rel_time"] + ["k_%d" % (i+1) for i in range(s-1)]
  310. fy = [x for x in f.getInputPortNames() if x not in ["time", "rel_time"]]
  311. input_ports += fy
  312. K = CBD("RK-K_%d" % s, input_ports, [])
  313. K.addBlock(f)
  314. # Time parameter
  315. K.addBlock(ConstantBlock("C", self._tableau.getNodes()[s-1]))
  316. K.addBlock(ProductBlock("CMult"))
  317. K.addBlock(AdderBlock("CSum"))
  318. K.addConnection("h", "CMult")
  319. K.addConnection("C", "CMult")
  320. K.addConnection("time", "CSum")
  321. K.addConnection("CMult", "CSum")
  322. K.addConnection("CSum", f.getBlockName(), "time")
  323. K.addConnection("rel_time", f.getBlockName(), "rel_time")
  324. # Y parameters
  325. if s - 1 > 0:
  326. K.addBlock(AdderBlock("KSum", s - 1))
  327. for i in range(s-1):
  328. j = i + 1
  329. K.addBlock(ConstantBlock("A_%d" % j, self._tableau.getA(s-1, j)))
  330. K.addBlock(ProductBlock("Mult_%d" % j))
  331. K.addConnection("A_%d" % j, "Mult_%d" % j)
  332. K.addConnection("k_%d" % j, "Mult_%d" % j)
  333. K.addConnection("Mult_%d" % j, "KSum")
  334. for y in fy:
  335. K.addBlock(AdderBlock("YSum-%s" % y))
  336. K.addConnection(y, "YSum-%s" % y)
  337. K.addConnection("KSum", "YSum-%s" % y)
  338. K.addConnection("YSum-%s" % y, f.getBlockName(), y)
  339. else:
  340. for y in fy:
  341. K.addConnection(y, f.getBlockName(), y)
  342. # Finishing Up
  343. outputs = f.getSignals().keys()
  344. for j, y in enumerate(outputs):
  345. i = j + 1
  346. K.addOutputPort("OUT%d" % i)
  347. K.addBlock(ProductBlock("FMult_%d" % i))
  348. K.addConnection("h", "FMult_%d" % i)
  349. K.addConnection(f.getBlockName(), "FMult_%d" % i, output_port_name=y)
  350. K.addConnection("FMult_%d" % i, "OUT%d" % i)
  351. return K
  352. def create_Error(self, vlen):
  353. r"""
  354. Creates the error computation block, which computes:
  355. .. math::
  356. h_{new} = S\cdot h_{old}\cdot\left(\dfrac{\epsilon\cdot h_{old}}{\vert z_{n+1} - y_{n+1}\vert}\right)^{\dfrac{1}{q}}
  357. Where :math:`\epsilon` is the provided error tolerance, :math:`q` the lowest order of the computation,
  358. :math:`z_{n+1}` the higher-order (more precise) value and :math:`y_{n+1}` the lower-order computation
  359. that will also be outputted. When :math:`y` and :math:`z` consist of multiple elements, a pessimistic
  360. approach is used, obtaining the maximal error.
  361. See Also:
  362. `Press, William H., H. William, Saul A. Teukolsky, A. Saul, William T. Vetterling, and Brian P. Flannery.
  363. 2007. "Numerical recipes 3rd edition: The art of scientific computing", Chapter 16, pp. 714-722.
  364. Cambridge University Press. <https://people.cs.clemson.edu/~dhouse/courses/817/papers/adaptive-h-c16-2.pdf>`_
  365. """
  366. Err = CBD("error", ["h"] + ["y_%d" % (i+1) for i in range(vlen)] + ["z_%d" % (i+1) for i in range(vlen)],
  367. ["h_new", "error"])
  368. Err.addBlock(MaxBlock("Max", vlen))
  369. for i in range(vlen):
  370. j = i + 1
  371. Err.addBlock(NegatorBlock("Neg_%i" % j))
  372. Err.addBlock(AdderBlock("Sum_%i" % j))
  373. Err.addBlock(AbsBlock("Abs_%i" % j))
  374. Err.addConnection("y_%d" % j, "Neg_%d" % j)
  375. Err.addConnection("Neg_%d" % j, "Sum_%d" % j)
  376. Err.addConnection("z_%d" % j, "Sum_%d" % j)
  377. Err.addConnection("Sum_%d" % j, "Abs_%d" % j)
  378. Err.addConnection("Abs_%d" % j, "Max")
  379. Err.addBlock(ProductBlock("Mult"))
  380. Err.addBlock(ProductBlock("Frac", 3))
  381. Err.addBlock(ConstantBlock("S", self._safety))
  382. Err.addBlock(ConstantBlock("q", self._tableau.getOrder()))
  383. Err.addBlock(InverterBlock("Inv"))
  384. Err.addBlock(ConstantBlock("Eps", self._tolerance))
  385. Err.addBlock(RootBlock("Root"))
  386. Err.addBlock(ProductBlock("MultH"))
  387. Err.addBlock(ClampBlock("Clamp", 0.1, 4.0))
  388. Err.addBlock(ClampBlock("ClampH", self._h_range[0], self._h_range[1]))
  389. Err.addConnection("h", "MultH")
  390. Err.addConnection("S", "Mult")
  391. Err.addConnection("Max", "Inv")
  392. Err.addConnection("Eps", "Frac")
  393. Err.addConnection("Inv", "Frac")
  394. Err.addConnection("h", "Frac")
  395. Err.addConnection("Frac", "Root", input_port_name="IN1")
  396. Err.addConnection("q", "Root", input_port_name="IN2")
  397. Err.addConnection("Root", "Mult")
  398. Err.addConnection("MultH", "ClampH")
  399. Err.addConnection("ClampH", "h_new")
  400. Err.addConnection("Clamp", "MultH")
  401. Err.addConnection("Mult", "Clamp")
  402. Err.addConnection("Max", "error")
  403. # TODO: enforce positive tolerance
  404. # TODO: if Max <= Eps * h: don't progress time
  405. return Err
  406. if __name__ == '__main__':
  407. from CBD.preprocessing.butcher import ButcherTableau as BT
  408. from CBD.converters.CBDDraw import draw
  409. DELTA_T = 0.1
  410. class Test(CBD):
  411. def __init__(self, block_name):
  412. CBD.__init__(self, block_name, input_ports=[], output_ports=['y'])
  413. # Create the Blocks
  414. self.addBlock(IntegratorBlock("int"))
  415. self.addBlock(ConstantBlock("IC", value=(0)))
  416. self.addBlock(ProductBlock("mult"))
  417. self.addBlock(AdderBlock("sum"))
  418. self.addBlock(ConstantBlock("one", value=(1)))
  419. self.addBlock(ConstantBlock("delta_t", value=(DELTA_T)))
  420. # self.addBlock(IntegratorBlock("int2"))
  421. # self.addBlock(ConstantBlock("seven", 7))
  422. # self.addBlock(AdderBlock("sum7"))
  423. # Create the Connections
  424. self.addConnection("IC", "int", output_port_name='OUT1', input_port_name='IC')
  425. self.addConnection("int", "mult", output_port_name='OUT1')
  426. self.addConnection("int", "mult", output_port_name='OUT2')
  427. self.addConnection("int", "y", output_port_name='OUT1')
  428. self.addConnection("mult", "sum", output_port_name='OUT1', input_port_name='IN2')
  429. self.addConnection("one", "sum", output_port_name='OUT1', input_port_name='IN1')
  430. self.addConnection("sum", "int", output_port_name='OUT1', input_port_name='IN1')
  431. self.addConnection("delta_t", "int", output_port_name='OUT1', input_port_name='delta_t')
  432. # self.addConnection("int", "sum7")
  433. # self.addConnection("clock-clock", "sum7", output_port_name='rel_time')
  434. # self.addConnection("seven", "sum7")
  435. # self.addConnection("sum7", "int2")
  436. test = Test("Test")
  437. test.addFixedRateClock("clock", 0.1)
  438. prep = RKPreprocessor(BT.RKF45(), atol=2e-5, safety=.84)
  439. model = prep.preprocess(test)
  440. draw(model.findBlock("RK.error")[0], "test.dot")
  441. # model = Test("Test")
  442. from CBD.simulator import Simulator
  443. sim = Simulator(model)
  444. sim.setDeltaT(0.2)
  445. sim.run(1.4)
  446. s = model.getSignal("y")
  447. L = len(s)
  448. errs = model.findBlock("RK.error")[0].getSignal("error")
  449. hs = model.findBlock("RK.error")[0].getSignal("h_new")
  450. # errs = hs = s
  451. # print([x for _, x in errs])
  452. import numpy as np
  453. print("+------------+------------+------------+------------+------------+------------+")
  454. print("| TIME | VALUE | TAN | ERROR | OFFSET | DELTA |")
  455. print("+------------+------------+------------+------------+------------+------------+")
  456. for i in range(L):
  457. t, v = s[i]
  458. actual = np.tan(t)
  459. error = abs(actual - v)
  460. print("| {t:10.7f} | {v:10.7f} | {h:10.7f} | {e:10.7f} | {o:10.7f} | {d:10.7f} |"
  461. .format(t=t, v=v, h=actual, e=error, o=errs[i].value, d=hs[i].value))
  462. print("+------------+------------+------------+------------+------------+------------+")
  463. import matplotlib.pyplot as plt
  464. fig, ax = plt.subplots()
  465. ax.plot(np.arange(0.0, 1.4, 0.01), [np.tan(t) for t in np.arange(0.0, 1.4, 0.01)], label="tan(t)")
  466. ax.plot([t for t, _ in s], [v for _, v in s], label="estimate")
  467. ax.legend()
  468. plt.show()