turion/rhine

Q: Signal-dependent sampling schedules?

freckletonj opened this issue · 3 comments

From what I understand in the paper, clocks probably couldn't be coordinated if the sampling-rate were signal-dependent. But I thought I'd ask!

Ex 1. If simulating a collision, you could increase the sampling frequency as 2 objects near each other to make sure they don't pass through each other.

Ex 2. If working with high-frequency signals, you could change the sample-rate as a function of the highest-frequency component in the signal for the last 2 seconds. If slow-waves abound, sample infrequently.

EDIT: perhaps this is related? https://github.com/turion/rhine/pull/148/files

Yes, please do ask :) and thanks for asking! After all, I want the library to be useful not only for me, but for many people, so I always like to know what use cases and requirements others have.

In fact you can cobble something like this together already with the existing tools. Let me write some code which I haven't tested, but which I'm sure can be made to work.

Ex 1. If simulating a collision, you could increase the sampling frequency as 2 objects near each other to make sure they don't pass through each other.

Something like this was in fact proposed by @ivanperez-keera but we never published it, I believe. It could be implemented roughly like so (heavily inspired by Ivan's ideas):

-- Using https://hackage.haskell.org/package/monoid-extras-0.5.1/docs/Data-Monoid-Inf.html#t:Inf
-- This is a monoid that selects the earlier of two timestamps. Its neutral element is positive infinity.
type CollisionDetectionTimeDomain td = (Inf Pos td)

-- | In this state we remember when we need to sample the next step.
type CollisionDetectionT td m a = StateT (CollisionDetectionTimeDomain td) m a

-- | Decrease the next needed sampling time such that we sample at `td` or earlier.
needSampleAt :: td -> CollisionDetectionT td m ()
needSampleAt td = modify $ (<> Finite td)

-- | Use this in your signal functions whenever you have an indicator when to sample.
needSampleAtS :: BehaviourF (CollisionDetectionT td m a) td td ()
needSampleAtS = arrMCl needSampleAt

-- | Listens to the next necessary sampling time and samples there.
data CollisionDetectionClock td = CollisionDetectionClock
  { maxStep :: td -- ^ Even if no earlier sampling was requested, two steps should never be further apart than this.
  }

instance Num td => Clock (CollisionDetectionT td m a) (CollisionDetectionClock td) where
  type Time td = td
  type Tag td = ()
  initClock CollisionDetectionClock { .. } = do
    let runningClock = proc _ -> do
       nextTime <- constM get -< () -- Collect the minimum of all the votes when the next sample should happen
       arrM put -< nextTime + maxStep -- Latest possible time to sample the next step
       returnA -< (nextTime, ()) -- Return earliest time when to sample now
    return (runningClock, 0)

This would implement a pure clock. For a realtime clock, you'd need to add the appropriate threadDelay or similar in order to block until the relevant time is reached.

If this is what you were looking for, and you have some motivation, feel free to make a pull request out of it, and we can make it work :)

Ex 2. If working with high-frequency signals, you could change the sample-rate as a function of the highest-frequency component in the signal for the last 2 seconds. If slow-waves abound, sample infrequently.

Similar in spirit to the previous example, you could use a Max monoid for a type of frequencies bounded from below, let parts of the signal functions vote on the sampling frequency, and then the clock reads from the state monad to achieve that frequency.

EDIT: perhaps this is related? https://github.com/turion/rhine/pull/148/files

Not directly. That's more a refactoring, to make type signatures and combinators simpler. (In my opinion one of the biggest improvements in rhine would be to simplify these.) But the simplifications in there would also make the answers to your questions here easier.

Fantastic! I'm wondering if, once polished, things like this should be in a rhine-contrib / rhine-community, or something? I'm really glad this is possible!

I'm wondering if, once polished, things like this should be in a rhine-contrib / rhine-community, or something?

No, I think they should go to the main library. My layout is: Everything that needs only core dependencies (base, well-established packages, no external libraries) goes into rhine. Everything that needs external dependencies like video, audio, some device etc., needs to be in a separate library. That way, the main library rhine will always build without surprises on all OSes.