One advantage of probabilistic programming in a general purpose language is that we can import existing libraries as part of our models. Here we follow an example from the Anglican team with a simple 2d physics problem based on the Chipmunk library. For the Anglican example see the Machine Learning Summer School or the KAIST lecture course. For more advanced physics simulations in probabilistic programming, see PPX.
Thanks also to Alexander Bai for initially adapting the Haskell implementation in Sam’s OPLSS course to LazyPPL.
The source for this Literate Haskell file is currently here.
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
module Physics where
import Apecs.Physics
import Apecs.Physics.Gloss
import Graphics.Gloss.Export (exportPicturesToGif,GifLooping(LoopingForever))
import Control.Monad (replicateM, when)
import LazyPPL
import LazyPPL.Distributions (uniformbounded, normalPdf)
import Numeric.Log(Log(Exp),ln)
import System.Random (setStdGen, mkStdGen)
import Graphics.Matplotlib hiding (density)
import System.IO.Unsafe (unsafePerformIO)
import Data.List (findIndex)
import Data.Monoid (Product(Product))
The general idea is:
There is a ball falling from a fixed position.
There is a cup at a fixed position.
We want to place and orient bumpers so that the ball falls in to the cup.
Here are four possible solutions, found by LazyPPL, when there are two bumpers:
"World" [''Physics, ''Camera] makeWorld
= do
initialize bumpers Camera (V2 0 1) 60
set global (
, earthGravity )
-- The bumpers
mapM (\(x,y,theta) -> do
<- newEntity (StaticBody, Angle (theta), Position (V2 x y))
lineBody Shape lineBody (hLine 2), Elasticity 0.3)
newEntity (
) bumpers
-- The cup
<- newEntity (StaticBody, Position (V2 4.5 (-5)))
lineBody Shape lineBody (hLine 1), Elasticity 0.1)
newEntity (<- newEntity (StaticBody, Position (V2 4 (-4.5)))
lineBody Shape lineBody (vLine 1), Elasticity 0.1)
newEntity (<- newEntity (StaticBody, Position (V2 5 (-4.5)))
lineBody Shape lineBody (vLine 1), Elasticity 0.1)
newEntity (
-- Border
<- newEntity (StaticBody, Position (V2 0 6))
lineBody Shape lineBody (hLine 12), Elasticity 0)
newEntity (<- newEntity (StaticBody, Position (V2 0 (-6)))
lineBody Shape lineBody (hLine 12), Elasticity 0)
newEntity (<- newEntity (StaticBody, Position (V2 6 0))
lineBody Shape lineBody (vLine 12), Elasticity 0)
newEntity (<- newEntity (StaticBody, Position (V2 (-6) 0))
lineBody Shape lineBody (vLine 12), Elasticity 0)
newEntity (
-- The ball
<- newEntity (DynamicBody, Position (V2 (-4.5) 5))
ball Shape ball (cCircle 0.2), Density 1, Elasticity 0.9)
newEntity (return ball
endCriterion :: (Double,Double) -> Bool
= y< -5 || (y < -4.5 && x > 4 && x < 5) endCriterion (x,y)
Make an animated gif from a sequence of pictures using Gloss.
= do
exportGif filename pics putStrLn $ "Plotting GIF to " ++ (show filename) ++ "..."
2 LoopingForever (288,288) (makeColor 1 1 1 0) filename (\t -> pics !! (floor t)) [0..(fromIntegral $ length pics - 1)]
exportPicturesToGif putStrLn $ "Done."
plotCoords :: String -> [[(Double,Double)]] -> Double -> Double -> Double -> IO ()
=
plotCoords filename xyss ymin ymax alpha do putStrLn $ "Plotting " ++ filename ++ "..."
$ mp # "plot.axis('off')" % mp # "colors = plot.cm.gnuplot2(np.linspace(0,1," # (floor ((fromIntegral $ length xyss) * 1.5) :: Integer) # ")) " % foldl (\a i -> a % plot (map fst (xyss !! i)) (map snd (xyss !! i)) @@ [o1 "o-", o2 "color" (lit ("colors[" ++ (show (length xyss - i)) ++ "]")), o2 "linewidth" (1.0 :: Double), o2 "alpha" alpha, o2 "ms" (0 :: Int)]) mp [0..(length xyss -1)] % (plot ([4,4,5,5] :: [Double]) ([-4,-5,-5,-4] :: [Double]) @@ [o1 "o-", o2 "c" "black", o2 "ms" (0 :: Int)] % xlim (-5 :: Int) (5.1 :: Double) % ylim ymin ymax)
file filename putStrLn "Done."
return ()
getTrajectory :: [(Double,Double,Double)] -> IO [(Double,Double)]
=
getTrajectory bumpers do w <- initWorld
$ do
runWith w <- initialize bumpers
b <- run b 500
xys return xys
where
-- run produces the trajectory until the endCriterion
-- or until the fuel runs out
-- (in case the ball somehow gets stuck)
= do
run b fuel 1/60)
stepPhysics (Position (V2 x y)) <- get b
(if fuel < 0 then return [(x,y)] else
if endCriterion (x,y) then return [(x,y)] else
do
<- run b (fuel - 1)
rest return $ (x,y) : rest
getPics :: [(Double,Double,Double)] -> IO [Picture]
=
getPics bumpers do w <- initWorld
$ do
runWith w <- initialize bumpers
b <- mapM (\_ -> do
xypics 1/60)
stepPhysics (Position (V2 x y)) <- get b
(<- draw
pic return (x,y,pic)
1..1000]
) [let (Just n) = findIndex (\(x,y,_) -> endCriterion (x,y)) xypics
return $ map (\(_,_,pic) -> pic) $ take (n + 50) xypics
where
= do
draw -- draw the current scene
<- foldDrawM drawBody
pic let cam = Camera (V2 (8) (-8)) 9
return $ cameraTransform cam pic
The model:
Pick some bumper positions and angles uniformly
Run the 2d physics
Then observe that the ball lands more-or-less in the cup.
Return a posterior distribution over bumper positions and angles.
model :: Meas [(Double,Double,Double)]
= do
model -- Pick the positions and angles of two bumpers
-- Prior is uninformative, uniformly distributed
<- sample $ replicateM 2 $ do
bumpers <- uniformbounded (-5) 5
x <- uniformbounded (-5) 5
y <- uniformbounded 0 pi
theta return (x,y,theta)
-- Run the physics simulation.
-- The last point is where the ball either leaves the scene
-- or lands in the cup.
let (x,y) = unsafePerformIO $
do { pos <- getTrajectory bumpers ; return $ last pos }
-- Observe the ball is roughly in the cup at the end of the trajectory
$ normalPdf 4.5 0.2 x
score -- Return the bumper positions
return bumpers
Note that we do not say that the ball lands exactly in the cup. We
could consider an alternative model by replacing the score with
score $ fromEnum (x > 4 && x < 5)
. This would
lead to Metropolis-Hastings rejecting the first samples, finding the
first success by brute force, which takes a
lot longer.
= do
runModelMH n trajfile anifile -- Use MH with p=0.15 (0.15 ~= 1/6, and approx six samples needed)
<- mh 0.15 model
samples -- The ball enters the cup when the likelihood is > 0.0876
let p = Product $ Exp $ ln $ 0.0876415
let (Just i) = findIndex (\(_,w) -> w > p) samples
putStrLn $ "[MH] Ball entered the cup after " ++ show i ++ " samples."
-- Output illustrations to files:
/= "") $ do
when (trajfile -- Extract the trajectories of the first n samples
<- mapM getTrajectory (map fst $ take n samples)
xyss -- Plot the trajectories together
-5) 5 0.01
plotCoords trajfile xyss (/= "") $ do
when (anifile -- Plot the last trajectory
<- getPics $ fst $ samples !! n
pics exportGif anifile pics
The four simulations at the top of the page were found by Metropolis-Hastings simulation.
Running the model with the LazyPPL Metropolis-Hastings gives trajectories like this (this is 1000 samples; the very last sample is the bottom-right animation at the top of the page):
The lighter colors (pink) are earlier samples from the Markov chain. We can see the “burn-in” process as the initial samples have low likelihood: they do not land in the cup. The ball begins landing in the pot after around 100 samples (68, 109, 118, 163 in the above animations).
The entire distribution will eventually be explored but only asymptotically. For simplicity we just restarted the simulation to get the four different animations at the top of the page, rather than waiting for one M-H chain to randomly enter many different modes.
= do
runModelBruteForce n trajfile <- weightedsamples model
samples -- The ball enters the cup when the likelihood is > 0.0876
let p = Exp $ ln $ 0.0876415
let (Just i) = findIndex (\(_,w) -> w > p) samples
putStrLn $ "[BF] Ball entered the cup after " ++ show i ++ " samples."
-- Extract the trajectories of the first n samples
<- mapM getTrajectory (map fst $ take n samples)
xyss -- Plot the trajectories together
-5) 5 0.1 plotCoords trajfile xyss (
Running the model with a brute-force search takes many more samples than Metropolis-Hastings. In the following illustration there were 3343 samples before the ball landed in the cup. Here are 4000 samples:
main :: IO ()
= do
main -- The distribution is multimodal,
-- so let's restart four times for four examples.
-- (Statistically better to use mhirreducible.)
-- We plot the trajectories of the last run.
1)
setStdGen (mkStdGen 1000 "" "images/physics1.gif"
runModelMH 42)
setStdGen (mkStdGen 1000 "" "images/physics2.gif"
runModelMH 47)
setStdGen (mkStdGen 1000 "" "images/physics3.gif"
runModelMH 57)
setStdGen (mkStdGen 1000 "images/physics4.svg" "images/physics4.gif"
runModelMH -- We also plot what brute force looks like with 4000 trials
4000 "images/physics5.svg" runModelBruteForce