packages feed

units-2.1: Tests/Compile/Simulator.hs

{- Copyright (c) 2013-4 Richard Eisenberg

This file demonstrates some of `units`'s capabilities by building up a simple
physics simulator.
-}

{-# LANGUAGE TypeOperators, TypeFamilies, QuasiQuotes #-}

module Tests.Compile.Simulator where

import Data.Metrology.SI
import Data.Metrology.Show ()
import Data.Metrology.Vector

-- We never want to add positions! QPoint protects us from this.
type Position = QPoint Length

-- +x = right
-- +y = up

-- We still want the "outer" type to be Qu, not the pair. So push the pairing
-- operation down to the Qu's representation.
type family Vec2D x where
  Vec2D (Qu d l n) = Qu d l (n, n)

-- An object in our little simulation
data Object = Object { mass :: Mass
                     , rad  :: Length
                     , pos  :: Vec2D Position
                     , vel  :: Vec2D Velocity }
  deriving Show

type Universe = [Object]

-- updating takes two passes: move everything according to their own positions
-- and gravity (ignoring pulls between objects), and then look for collisions
-- and update collided objects' positions and velocities accordingly. This might
-- fail if three objects were to collide in a row all at once.

g :: Vec2D Acceleration
g = (0,-9.8)% [si| m/s^2 |]  -- could also be Meter :/ (Second :^ sTwo)

g_universe :: Force %* Length %^ Two %/ (Mass %^ Two)
g_universe = 6.67e-11 % [si| N m^2 / kg^2 |]

update :: Time -> Universe -> Universe
update dt objs
  = let objs1 = map (updateNoColls dt objs) objs in
    updateColls objs1

-- update without taking collisions into account
updateNoColls :: Time -> Universe -> Object -> Object
updateNoColls dt univ obj@(Object { mass = m, pos = x, vel = dx })
  = let new_pos = x |.+^| dx |^*| dt  -- new position
        v1 = dx |+| g |^*| dt         -- new velocity w.r.t. downward gravity
        f = gravityAt univ x m
        a = f |^/| m
        v2 = v1 |+| a |^*| dt         -- new velocity also with mutual gravity
    in obj { pos = new_pos, vel = v2 }

-- calculate the gravity at a point from all other masses
gravityAt :: Universe -> Vec2D Position -> Mass -> Vec2D Force
gravityAt univ p m = qSum (map gravity_at_1 univ)
  where
    -- gravity caused by just one point
    gravity_at_1 (Object { mass = m1, pos = pos1 })
      = let r = p |.-.| pos1
            f = g_universe |*| m1 |*| m |*^| r |^/| (qMagnitude r |^ sThree)
        in
        if qMagnitude r |>| (zero :: Length)  -- exclude the point itself!
        then redim f 
        else zero

-- update by collisions
updateColls :: Universe -> Universe
updateColls objs
  = let collisions = findCollisions objs in
    map resolveCollision collisions

-- returns a list of collisions, as pairs of an object with, perhaps,
-- a collision partner
findCollisions :: Universe -> [(Object, Maybe Object)]
findCollisions objs
  = map (findCollision objs) objs

-- check for collisions for one particular Object
findCollision :: Universe -> Object -> (Object, Maybe Object)
findCollision [] obj = (obj, Nothing)
findCollision (other : rest) obj
  | colliding other obj
  = (obj, Just other)
  | otherwise
  = findCollision rest obj

-- are two objects in contact?
colliding :: Object -> Object -> Bool
colliding (Object { pos = x1, rad = rad1 })
          (Object { pos = x2, rad = rad2 })
  = let distance = qDistance x1 x2 in
    distance |>| (zero :: Length) && distance |<=| (rad1 |+| rad2)

-- resolve the collision between two objects, updating only the first
-- object in the pair. The second object will be updated in a separate
-- (symmetric) call.
resolveCollision :: (Object, Maybe Object) -> Object
resolveCollision (obj, Nothing) = obj
resolveCollision (obj@Object { mass = m1, rad = rad1
                             , pos = z1, vel = v1 },
                Just (Object { mass = m2, rad = rad2
                             , pos = z2, vel = v2 }))
  = let -- c :: Vec2D Length
        c = z2 |.-.| z1   -- vector from z1 to z2

        -- vc1, vc2, vd1, vc1', v1' :: Vec2D Velocity
        vc1 = c `qProject` v1  -- component of v1 along c
        vc2 = c `qProject` v2  -- component of v2 along c
        vd1 = v1 |-| vc1       -- component of v1 orthogonal to c
        vc1' = (m1 |*^| vc1 |-| m2 |*^| vc2 |+| 2 *| m2 |*^| vc2) |^/| (m1 |+| m2)
                               -- new component of v1 along c
        v1' = vc1' |+| vd1     -- new v1

          -- also, move object 1 to be out of contact with object 2
        z1' = z2 |.-^| (rad1 |+| rad2) |*^| qNormalized c
    in
    obj { pos = z1', vel = v1' }