packages feed

simgi-0.2: src/Engine.hs

{-----------------------------------------------------------------
 
  (c) 2009 Markus Dittrich 
 
  This program is free software; you can redistribute it 
  and/or modify it under the terms of the GNU General Public 
  License Version 3 as published by the Free Software Foundation. 
 
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License Version 3 for more details.
 
  You should have received a copy of the GNU General Public 
  License along with this program; if not, write to the Free 
  Software Foundation, Inc., 59 Temple Place - Suite 330, 
  Boston, MA 02111-1307, USA.

--------------------------------------------------------------------}

-- | the main compute Engine
module Engine ( compute_trigger
              , create_initial_output
              , create_initial_state
              , execute_actions
              , gillespie_driver
              , module GenericModel
              ) where


-- imports
import Control.Monad.State
import qualified Data.Map as M
import Prelude
import Text.Printf
import System.Random(randomR)
import qualified System.Random.Mersenne.Pure64 as MT
import System.IO


-- local imports
import ExtraFunctions
import GenericModel
import RpnCalc


-- | main simulation driver
-- the simulator either stops when
-- 1) the number of iterations is exhausted
-- 2) the current time is > t_max, if t_max is set to
--    zero t_max is treated as being infinity 
gillespie_driver :: Handle -> Double -> Integer -> ModelState -> IO ()
gillespie_driver handle simTime dmpIter state =  
  let 
    (output, outState)  = runState run_gillespie $ state 
    (curTime, newState) = update_state dmpIter outState
    reversedOutput      = reverse output
  in
    -- write output to console and the output file
    (write_info $ head reversedOutput)
    >> (write_data handle reversedOutput)

    -- next iteration if we're not at the end
    >> if curTime < simTime
         then gillespie_driver handle simTime dmpIter newState
         else return ()



-- | updates the state for the next iteration
update_state :: Integer -> ModelState -> (Double,ModelState)    
update_state dataDumpIter 
             state@(ModelState { currentTime      = t 
                               , outputBufferSize = it
                               }) 
  = (t, state { outputBufferSize = it + dataDumpIter, outputCache = [] })



-- | actual compute loop
run_gillespie :: GillespieState [Output]
run_gillespie = get

  >>= \inState@(ModelState { molCount         = in_mols
                           , reactions        = in_reacts
                           , randGen          = rGen
                           , events           = molEvents
                           , currentTime      = t
                           , currentIter      = it
                           , maxTime          = t_max
                           , outputBufferSize = it_max
                           , outputFreq       = freq
                           , outputRequest    = outputVars
                           , outputCache      = output
                           , variables        = theVars
                           }) ->


    -- compute and update the next state
    let 
      -- generate two random numbers
      (r1,rGen1) = randomR (0.0 :: Double, 1.0) rGen
      (r2,rGen2) = randomR (0.0 :: Double, 1.0) rGen1

      -- update state
      symbols    = SymbolTable in_mols theVars
      out_rates  = compute_rates symbols in_reacts t []
      a_0        = sum out_rates
      tau        = (-1.0/a_0) * log(r1)
      t_new      = t+tau
      mu         = get_mu (a_0*r2) out_rates
      out_mols   = update_molcount in_mols in_reacts mu
      newSymbols = (symbols { molSymbols = out_mols })
      evt_syms   = handle_events molEvents newSymbols t_new
      new_output = generate_output freq it t_new newSymbols outputVars output
      newState   = inState { molCount    = (molSymbols evt_syms)
                           , rates       = out_rates
                           , randGen     = rGen2
                           , currentTime = t_new
                           , currentIter = it+1
                           , outputCache  = new_output
                           , variables   = (varSymbols evt_syms)
                           }
    in

    -- this prevents simulation from getting stuck
    -- FIXME: We need to come up with mechanism to propagate
    -- error message corresponding to cases such as this one 
    -- to the user!
    if (is_equal tau 0.0)
      then let finalState = newState { currentTime = t_max } in
           put finalState >> return output
      else 
        if ( it_max == it || t >= t_max )
          then return output
          else put newState >> run_gillespie



-- | handle all user defined events and return the adjusted
-- number of molecules
-- WARNING: We should probably check the Event Stack before we use
-- it to compute stuff; at least make sure molecule exist
handle_events :: [Event] -> SymbolTable -> Double -> SymbolTable
handle_events [] symbols     _ = symbols
handle_events (x:xs) symbols t = 
  let
    newSymbols = handle_single_event x symbols t
  in
    handle_events xs newSymbols t



-- | handle a single user event
handle_single_event :: Event -> SymbolTable -> Double -> SymbolTable
handle_single_event evt symbols t =
  let
    triggers     = evtTrigger evt
    actions      = evtActions evt
    triggerVal   = compute_trigger symbols t triggers
  in 
    if triggerVal
      then execute_actions actions symbols t 
      else symbols



-- | compute the value of a trigger
compute_trigger :: SymbolTable -> Double 
                -> ([EventTriggerPrimitive],[EventTriggerCombinator]) -> Bool
compute_trigger _       _ ([],_) = False -- this is should never happen
compute_trigger symbols t ((x:xs),combs) = compute_trigger_h (eval_trigger x) xs combs

  where
    compute_trigger_h acc []     _      = acc
    compute_trigger_h acc _      []     = acc
    compute_trigger_h acc (y:ys) (c:cs) = 

      case c of
        AndCombinator -> compute_trigger_h (acc && (eval_trigger y)) ys cs
        OrCombinator  -> compute_trigger_h (acc || (eval_trigger y)) ys cs

                                          
    eval_trigger e = (trigRelation e) (leftTrigger e) (rightTrigger e)
    leftTrigger    = rpn_compute symbols t . trigLeftExpr 
    rightTrigger   = rpn_compute symbols t . trigRightExpr



-- | handle all actions associated with a user event
execute_actions :: [EventAction] -> SymbolTable -> Double 
                -> SymbolTable
execute_actions [] symbols _     = symbols
execute_actions (x:xs) symbols t =
  let
    newSymbol = execute_single_action x symbols t
  in
    execute_actions xs newSymbol t



-- | handle a single event triggered action
execute_single_action :: EventAction -> SymbolTable -> Double
                      -> SymbolTable
execute_single_action eventAction symbols t =
  let
    aName  = evtName eventAction
    action = evtAct eventAction
  in
    case action of
      Constant c   -> adjust_count aName c 

      Function rpn -> let
                        newCount = rpn_compute symbols t rpn
                      in
                        adjust_count aName newCount 
  
      where
        -- NOTE: presently, converting double -> int is done
        -- via floor. Is this a good policy (once documented
        -- properly)?
        to_int :: Double -> Int
        to_int = floor 

        -- adjust either a molecule count or the value of a
        -- variable
        adjust_count key val = case M.member key (molSymbols symbols) of

          True  -> symbols { molSymbols = 
                             M.insert key (to_int val) (molSymbols symbols) }
          False -> symbols { varSymbols = 
                             M.insert key (Constant val) (varSymbols symbols) }



-- | generate a new Output data structure based on the current
-- molecule counts
generate_output :: Integer -> Integer -> Double -> SymbolTable
                -> [String] -> [Output] -> [Output]
generate_output afreq it t symTable outVars outlist  
  | mod it afreq /= 0  = outlist
  | otherwise          = new_out:outlist

    where
      currentOutputList = grab_output_data outVars t symTable

      new_out = Output { iteration  = it
                       , time       = t
                       , outputData = currentOutputList
                       }



-- | given a list of variable or molecule names, goes through the
-- symbol table, grabs the current values associated with the variables,
-- and returns them as a list
grab_output_data :: [String] -> Double -> SymbolTable -> [Double]
grab_output_data vars aTime symbols = 
  foldr (\x acc -> (get_val_from_symbolTable x aTime symbols):acc) [] vars
                                


-- | depending on which reaction happened adjust the number of 
-- molecules in the system
update_molcount :: MoleculeMap -> [Reaction] -> Int -> MoleculeMap
update_molcount theMap rs mID =

  let 
    (Reaction { reaction = react_in }) = rs !! mID
  in
    adjustMap react_in theMap 

  where
    adjustMap :: [(String,Int)] -> MoleculeMap -> MoleculeMap
    adjustMap [] m = m
    adjustMap ((k,a):changes) m = let 
                                    val   = (M.!) m k
                                    m_new = M.insert k (a+val) m
                                  in
                                    adjustMap changes m_new



-- | pick the \mu value for the randomly selected next reaction 
-- reaction to happen
get_mu :: Double -> [Double] -> Int
get_mu val = length . takeWhile ( <val ) . scanl1 (+) 



-- | compute the current value for the reaction probabilities based 
-- on the number of molecules and reaction rates
compute_rates :: SymbolTable -> [Reaction] -> Double 
              -> RateList -> RateList
compute_rates _ [] _ rts = reverse rts
compute_rates symbols ((Reaction {rate = c_in, actors = a_in }):rs) 
  theTime rts = 
  
  case c_in of
    (Constant aRate)    -> compute_rates symbols rs theTime
       ((a_new aRate): rts)
    (Function rateFunc) -> compute_rates symbols rs theTime
       ((a_new . (rpn_compute symbols theTime) $ rateFunc):rts)
 
  where
    mult  = product $ map (\(a,f) -> f . fromIntegral $ 
            (M.!) (molSymbols symbols) a) a_in 
    a_new = (*) mult 
    



-- | initialize the output data structure
create_initial_output :: ModelState -> Output
create_initial_output (ModelState { molCount      = initialMols 
                                  , variables     = initialVars
                                  , outputRequest = outVars
                                  }) =
  
  Output { iteration = 1
         , time      = 0.0
         , outputData = initialOutput
         }


  where
    symbols = SymbolTable initialMols initialVars
    initialOutput = grab_output_data outVars 0.0 symbols



-- | set up the initial state
create_initial_state:: ModelState -> Output -> ModelState
create_initial_state state@(ModelState { seed = theSeed}) out = 

  state { rates       = defaultRateList
        , randGen     = MT.pureMT theSeed
        , currentTime = 0.0
        , currentIter = 1
        , outputCache = [out]
        }



-- | routine for writing basic accounting info to stdout
write_info :: Output -> IO ()
write_info (Output {iteration = it, time = t}) = 
    putStrLn $ printf "iteration: %-10d  --> time: %6.5g s" it t 



-- | basic routine writing the simulation output to the 
-- file handle corresponding to the output file
write_data :: Handle -> [Output] -> IO ()
write_data _ [] = return ()
write_data handle ((Output {iteration = it, time = t, outputData = out}):xs) = 

  let 
    header = (printf "%-10d %18.15g  " it t) :: String
    counts = create_count_string out
  in
    hPutStrLn handle (header ++ counts)
    >> write_data handle xs

  where
    create_count_string :: [Double] -> String
    create_count_string = foldr (\x a -> (printf "%18.15f  " x) ++ a) ""