(defn stream-integrator
"Produces a function, monotonic in its single numeric argument,
that represents the integral of the function f' given the initial
data $y_0 = f(x_0)$ and an options dictionary (presently containing
the tolerance for error $\\epsilon$, but eventually also selecting
from a menu of integration techniques).
This is done by creating an adaptive step-size ODE solver, and
advancing its steps as needed to supply function values. (This
architecture accounts for why the arguments to f must be presented
in order). Old solution segments are discarded. The goal of this
approach is to avoid the requirement of supplying an upper limit
to the integration. At the cost of requiring monotonic arguments
to f, the integrated function can essentially be used forever
without accumulating unbounded state.
The function `f'` should have the signature `[x y y']`, where `y'` is a
primitive double array, which the function should fill in based
on the values `x` and `y`.) Both `y` and `y'` will be primitive arrays
of type double, the same length as that of `y0`. Both arrays are
owned by the integrator. In particular, y should never be modified,
and neither array should be modified or expected to persist after
the return of `f'`. This approach has observable memory and
performance impacts.
The return value of the integrating function, however, is newly
allocated and belongs to the caller.
The integrating function may be called with no arguments to shut down
the integration, allowing for the final reclamation of its resources.
When the ODE solver is provided by Java, it may be necessary to
use an auxiliary thread to enable this style of flow control. If
JavaScript, we expect the solver to provide a generator of solution
segments."
[f' x0 y0 {:keys [epsilon #?(:cljs js?)]
:or {epsilon default-epsilon
#?@(:cljs [js? false])}}]
(let [dimension (count y0)]
#?(:clj
(let [gbs (GraggBulirschStoerIntegrator. 0. 1. (double epsilon) (double epsilon))
ode (ExpandableStatefulODE.
(reify FirstOrderDifferentialEquations
(computeDerivatives [_ x y out] (f' x y out))
(getDimension [_] dimension)))
step-requests (a/chan)
solution-segments (a/chan)]
(doto ode
(.setTime x0)
(.setPrimaryState (double-array y0)))
(.addStepHandler
gbs
(reify StepHandler
(init [_ _ _ _])
(handleStep [_ interpolator _]
(when-not (and (a/>!! solution-segments
(let [x0 (.getPreviousTime interpolator)
x1 (.getCurrentTime interpolator)]
{:x0 x0
:x1 x1
:f (fn [x]
(assert (and (>= x x0)
(<= x x1))
(format "ODE interpolation request %g out of range [%g, %g]"
x x0 x1))
(.setInterpolatedTime interpolator x)
(.getInterpolatedState interpolator))}))
(a/<!! step-requests))
(u/interrupted "end of integration")))))
(doto (Thread.
(fn []
(when-not (a/<!! step-requests)
(u/interrupted "end of integration"))
(try
(.integrate gbs ode Double/MAX_VALUE)
(catch InterruptedException _)
(catch Throwable t
(a/>!! solution-segments t)))))
(.setDaemon true)
(.start))
(let [next-segment (fn []
(a/>!! step-requests true)
(let [v (a/<!! solution-segments)]
(when (u/throwable? v)
(throw v))
v))
current-segment (atom (next-segment))
evaluate-at (fn [x]
(when (< x (:x0 @current-segment))
(u/illegal-state "Cannot use interpolation function in backwards direction"))
(while (> x (:x1 @current-segment))
(let [s (next-segment)]
(reset! current-segment s)))
((:f @current-segment) x))]
(fn f
([]
(a/>!! step-requests false))
([x]
(into [] (evaluate-at x)))
([x ^doubles out]
(System/arraycopy (evaluate-at x) 0 out 0 dimension)))))
:cljs
(let [solver (o/Solver.
f'
dimension
#js {:absoluteTolerance epsilon
:relativeTolerance epsilon
:rawFunction true})
f (.integrate solver x0 (double-array y0))]
(if js?
f
(comp js->clj f))))))