Sticky Platelets in a Pipeline (Part 2)
In my last post I showed the infrastructure and logic of a simple simulation of some sticky blood platelets moving through a blood vessel. I added the video to the end of that post; in this post I will explain how the visualisation was implemented.
In most CHP simulations there is a barrier (typically named tick) that is used to keep all the simulation entities synchronised, so that they all proceed through the discrete time-steps together. When it comes to visualising simulations, we could add a separate channel through which to report the status of all the simulation entities to a central drawing process, but this feels like a waste when all the processes are already synchronising together. CHP has reduce channels (many-to-one) that are the opposite of (one-to-many) broadcast channels; in a reduce channel, all the writers send in their data to a single reader, and all must send together before the communication is complete. So a simple way to achieve visualisation is to transform the tick barrier into a reduce channel, which has the same synchronisation behaviour, but that also allows data to be sent to some drawing process.
The simulation entities don’t need to know the details of all this, so we hide it using a send action that I’ve called a tick action:
type TickAction = SendAction (Maybe (Platelet, Bool)) data Platelet = Platelet GLfloat
Note that I’ve also had to tweak the type in the platelet from last time to make working with the OpenGL library a bit easier. The only change to the site process (and plateletGenerator and plateletPrinter processes) is related to the change in the tick event to being an action. Here is the site process in full:
site :: Chanin (Maybe Platelet) -> Chanout (Maybe Platelet) -> TickAction -> CHP () site prevIn nextOut act = foreverFeed (maybe empty full) Nothing where tickWith = sendAction act empty = join . fst <$> offer ( (once $ readChannel prevIn) `alongside` (once $ writeChannel nextOut Nothing) `alongside` (endWhen $ tickWith Nothing) ) full platelet = do r <- liftIO $ randomRIO (0, (1::Double)) let moving = r >= 0.05 tick = tickWith (Just (platelet, moving)) mv = readChannel prevIn <&> writeChannel nextOut (Just platelet) probablyMove = if moving then fst <$> mv else stop fromMaybe (Just platelet) . fst <$> offer ((once probablyMove) `alongside` endWhen tick)
Synchronising on the tick barrier has become engaging in the tick event, where we must pass in that optional pair Maybe (Platelet, Bool); the first item is the platelet currently occupying the site, and the boolean indicates whether the platelet was willing to move on this time-step. If the site is empty, the Nothing value is sent.
The status values end up passed to the draw function, which is fairly boring (it draws a vertical bar) — we are only interested in its type:
data ScreenLocation = ScreenLocation Int deriving (Eq, Ord) draw :: ScreenLocation -> Maybe (Platelet, Bool) -> IO ()
This is called from the drawProcess:
drawProcess :: ReduceChanin (Map.Map ScreenLocation (Maybe (Platelet, Bool))) -> CHP () drawProcess input = do displayIO <- embedCHP_ $ do x <- readChannel input liftIO $ do startFrame mapM_ (uncurry draw) (Map.toList x) endFrame ...
Note that drawProcess takes the input end of a reduce channel which carries a map from ScreenLocation (a one-dimensional location in our simple example) to the Maybe (Platelet, Bool) values we saw. Reduce channels in CHP must carry a monoid type, because the monoid instance is used to join all the many values from the writers into a single value (using mappend/mconcat, but in a non-deterministic order — so make sure the monoid is commutative).
The Map type (from Data.Map) is a monoid that has union as its mappend operation. This is exactly what we want; each site process will send in a singleton Map with their specific screen location mapped to their current status, and using the monoid instance these maps will all be joined (quite safely, since each sender will have a different location, and hence a distinct key entry in its Map) into one big map, that can then be fed into the draw function as we saw above.
We don’t trouble the site process with knowing its location; instead, we wrap up the location in the send action. It is easy to construct send actions that apply a function to a value before it is sent, so we apply a function that takes a status value, and turns it into the singleton Map just discussed. This is all done as part of the wiring up of the process network, based on the version from last time:
main :: IO () main = runCHP_ $ do status <- manyToOneChannel pipelineConnectCompleteT (enrollAll (return $ writer status) . zipWith withSend locationList) plateletGenerator (replicate numSites site) plateletPrinter <|*|> drawProcess (reader status) where numSites = screenSize - 2 locationList = map ScreenLocation [0..(screenSize-1)] withSend k p c = p $ makeSendAction' c (Map.singleton k)
The withSend function does the wrapping of the modification function with the send action. Each site is given a location from the list, including the generator and printer processes; otherwise this function is the same as the version from part 1.
I’ve omitted the OpenGL code, which is much the same as my previous examples. But here, again, is the video showing the results of the visualisation:
Turning the tick barrier into a reduce channel is often an easy way to visualise a simulation, and doesn’t require too much change to the code. As I said last time, the video is nothing compared to the TUNA videos which are very impressive, and some of which were generated by distributing the code over a cluster — a topic I hope to come to in CHP at some point in the future.