turion/rhine

Q: What if a computation is slower than the sample rate?

freckletonj opened this issue · 3 comments

Ex: a sample rate of 10 Hz, but the intervening computation takes say 3s?

Maybe I missed it in the paper, but I could see 2 solutions:

  1. Realtime solution : a resampling strategy that allows to "ignore clock ticks" if a separate thread is still computing a result from a previous tick. It could interpolate a result for a computation that we didn't have time to actually calculate.

  2. Simulation-time-accurate : A slow computation blocks the entire clock tree. So if you want to simulate 10 x 10Hz samples and each computation takes 3s, the Rhine will take 30s to run, but the result will in-spirit represent 1s of simulation.

This is one of the ramifications that I unfortunately couldn't go into in the article, but it's an important question. It depends a bit on whether the computation can be run concurrently. If yes, you can use EventClock http://hackage.haskell.org/package/rhine-0.7.0/docs/FRP-Rhine-Clock-Realtime-Event.html. Also, it depends on whether your computation is slow because it blocks on some device access or because it needs to do heavy calculations. And it depends on what you want to achieve. Is it ok if the computation delays the clock, i.e. do we have to achieve the sample rate?

So maybe you can specify a particular use case, and then I can tell you more precisely what would be the right approach?

In the meanwhile I'm going to throw some ideas around how to achieve roughly what you proposed, maybe it's what you're looking for.

  • Idea 1a (Realtime): You have one rhine slowComputation >-> emitS' @@ MilliSecond 100 which starts a computation but doesn't fully evaluate the thunk. Then in a separate rhine you have tagS >-> arr force >-> useComputation @@ EventClock which starts a computation whenever an event (= a thunk) arrives. Then you flow both rhines in a separate thread. As long as useComputation doesn't block the whole GHC runtime, the first rhine isn't influenced by the second.
  • Idea 1b (Realtime): The same ingredients, but you put both rhines in the same big signal network. The "disadvantage" is that in one rhine, by default all data-processing units are single-threaded. Only the clocks are launched in different threads (e.g. when using concurrently). Then the computation blocks the consumption of the next clock tick, and this will be reflected in the measured timestamps.
  • Idea 2 (Realtime): Put slowComputation >-> emitS' @@ Busy in the first thread, which evaluates its argument fully before sending it (And also tries to restart the computation again as soon as the first has finished). Then put tagS >-> (useComputation +++ somethingElseAt10Hz) @@ ParallelClock EventClock (MilliSecond 100) in the other thread, which is called every 100 ms, and additionally whenever a computation has finished.
  • Idea 3 (simulation time): As you suggest, you use a pure clock like FixedStep, and run the computation in there. You can use RescaledClock to convert the natural number of FixedStep into e.g. a double, Rational or UtcTime.

Awesome, that doesn't seem too bad! My usecase was initially just to digest the paper properly, but now knowing this isn't too tough, I may use it in robotics simulation and audio generation!

Thank you, I feel lucky to have this documentation to now refer back to :D

Great :) feel free to come back for any questions. If your use case is open source, feel free to ping me on a PR if you run into trouble, and I can fix it there.

I'll close now. Reopen for further questions.