Skip to content

Proper contexts has arrived!

Compare
Choose a tag to compare
@HugoGranstrom HugoGranstrom released this 22 Sep 14:32
· 161 commits to master since this release
acad18e

In this update, ODE and integrate have gotten support for a context NumContext which provides a persistent storage between function calls. It can be used to pass in parameters or to save extra information during the processing. For example:

  • If you have a matrix you want to reuse often in your function, you can save it in the NumContext once and then access it with a key. This way you only have to evaluate it once instead of creating it on every function call or making it a global variable.
  • You want to modify a parameter during the processing and access the modified value during later calls.

Warning: NumContext is a ref-type. So it means that the context variable you pass in will be altered if you do anything to it in your function. The ctx in your function is the exact same as the one you pass in, so if you change it, you will also change the original variable.

What does this mean for your old code? It means you have to change the proc signature for your functions:

  1. ODE:
# Old code
proc f[T](x: float, y: T): T =
  # do your calculation here

# New code
proc(t: float, y: T, ctx: NumContext[T]): T =
  # do your calculations here
  1. integrate:
# Old code
proc(x: float, optional: seq[T]): T =
  # do your calculations here

# New code
proc(x: float, ctx: NumContext[T]): T =
  # do your calculations here

So how do we create a NumContext then? Here is an example:

import arraymancer, numericalnim
var ctx = newNumContext[Tensor[float]]()
# Values of type `T`, `Tensor[float]` in this case is accessed using `[]` with either a string or enum as key.
ctx["A"] = @[@[1.0, 2.0], @[3.0, 4.0]].toTensor
# `NumContext` does always have a float storage as well accessed using `setF` and `getF`.
ctx.setF("k", 3.14)
# it can then be accessed using ctx.getF("k")

As for passing in the NumContext you pass it in as the parameter ctx to the integration proc or solveODE:

proc f_integrate(x: float, ctx: NumContext[float]): float = sin(x)
let I = adaptiveGauss(f_integrate, 0.0, 2.0, ctx = ctx)

proc f_ode(t: float, y: float, ctx: NumContext[float]): float = sin(t)*y
let (ts, ys) = solveODE(f_ode, y0 = 1.0, tspan = [0.0, 2.0], ctx = ctx)

If you don't pass in a ctx, an empty one will be created for you but you won't be able to access after the proc has finished and returned the results.

Using enums as keys for NumContext

If you want to avoid KeyErrors regarding mistyped keys, you can use enums for the keys instead. The enum value will be converted to a string internally so there is no constraint that all keys must be from the same enum. Here is one example of how to use it:

type
  MyKeys = enum
    key1
    key2
    key3
var ctx = newNumContext[float]()
ctx[key1] = 3.14
ctx[key2] = 6.28