Now, let's try Paraiso in a real-world problem. Simulation of artificial life will be a good starting point.
The sample program, like every other Haskell programs, starts with some language extension and imports.
#!/usr/bin/env runhaskell {-# LANGUAGE NoImplicitPrelude, OverloadedStrings #-} {-# OPTIONS -Wall #-} import Control.Monad import Data.Tensor.TypeLevel import Language.Paraiso.Annotation (Annotation) import qualified Language.Paraiso.Annotation.Boundary as Boundary import Language.Paraiso.Generator (generateIO) import qualified Language.Paraiso.Generator.Native as Native import Language.Paraiso.Name import Language.Paraiso.OM import Language.Paraiso.OM.Builder import Language.Paraiso.OM.Builder.Boolean (select,eq,ge,le) import Language.Paraiso.OM.DynValue as DVal import Language.Paraiso.OM.Realm import qualified Language.Paraiso.OM.Reduce as Reduce import Language.Paraiso.OM.Value (StaticValue(..)) import Language.Paraiso.Optimization import Language.Paraiso.Prelude import NumericPrelude hiding ((||),(&&)) -- the main program main :: IO () main = do _ <- generateIO mySetup myOM return ()
Let's say our computation region to be 80x48 and with cyclic boundary conditions. The new Orthotope Machine will have the name "Life".
-- the code generation setup mySetup :: Native.Setup Vec2 Int mySetup = (Native.defaultSetup $ Vec :~ 80 :~ 48) { Native.directory = "./dist/" , Native.boundary = compose $ const Boundary.Cyclic } -- the orthotope machine to be generated myOM :: OM Vec2 Int Annotation myOM = optimize O3 $ makeOM (mkName "Life") [] myVars myKernels
We use an array variable cell
for the cell states, population
for counting the number of alive cells and generation
for keeping track of the time.
-- the variables we use cell :: Named (StaticValue TArray Int) cell = "cell" `isNameOf` StaticValue TArray undefined population :: Named (StaticValue TScalar Int) population = "population" `isNameOf` StaticValue TScalar undefined generation :: Named (StaticValue TScalar Int) generation = "generation" `isNameOf` StaticValue TScalar undefined myVars :: [Named DynValue] myVars = [f2d cell, f2d population, f2d generation]
Then, let's make two kernels, one for the initialization of the state and the other for updating the state for one generation.
-- our kernel myKernels :: [Named (Builder Vec2 Int Annotation ())] myKernels = ["init" `isNameOf` initBuilder, "proceed" `isNameOf` proceedBuilder]
Initialization, is just trivial.
initBuilder :: Builder Vec2 Int Annotation () initBuilder = do -- store the initial states. store cell 0 store population 0 store generation 0
Now, how shall we write the rule of Conway's game of Life? Let's first define the adjacency. In Conway's game of Life, one cell has eight neighbours:
adjVecs :: [Vec2 Int] adjVecs = zipWith (\x y -> Vec :~ x :~ y) [-1, 0, 1,-1, 1,-1, 0, 1] [-1,-1,-1, 0, 0, 1, 1, 1]
A timestep of the game begins by loading a Array variable called "cell" as the old state of the simulation.
proceedBuilder :: Builder Vec2 Int Annotation () proceedBuilder = do oldCell <- bind $ load cell
We also load a Scalar variable called "generation."
gen <- bind $ load generation
shiftedCell <- bind $ shift v oldCell
is an expression that yields a cell pattern shifted by amount v
from the old cell. Then mapping it over every v
in adjVecs
creates the neighbour list.
neighbours <- forM adjVecs $ \v -> bind $ shift v oldCell
Count the neighbour cells that are alive.
num <- bind $ sum neighbours
Apply the rule of Conway's game of Life.
isAlive <- bind $ (oldCell `eq` 0) && (num `eq` 3) || (oldCell `eq` 1) && (num `ge` 2) && (num `le` 3)
Update the cell according to the rule. The expression select isAlive 1 0
, means isAlive ? 1 : 0
in C.
newCell <- bind $ select isAlive 1 0
Count the number of alive cells, increment the generation, and store the new cell state.
store population $ reduce Reduce.Sum newCell store generation $ gen + 1 store cell $ newCell
No comments:
Post a Comment