WIP
Abstract: In this post we loosly follow (1) and implement some of the Bloch simulations in Haskell using the State Monad. The hvega
package using vega-lite
specifications is employed to recreate some of the same plots.
Keywords: MR physics, Bloch equations, Pulse sequences, Haskell, StateMonad, vega-lite
Preamble
We add this header to later be able to easily run the code as a script using stack
.
#!/bin/env stack
{- stack
script
--resolver lts-18.19
--package hmatrix
--package lens
--package mtl
--package aeson
--package hvega
-}
Imports
Language extensions and module imports.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Mag where
import Control.Lens hiding ( transform )
import Control.Monad.State.Lazy
import Data.Aeson ( encodeFile )
import Data.Bifunctor
import Data.List.NonEmpty ( NonEmpty(..) )
import qualified Data.List.NonEmpty as Lne
import Graphics.Vega.VegaLite
import Numeric.LinearAlgebra
import Prelude hiding ( (<>) )
Types
We also add some type aliases to have kind of self-documenting type signatures.
type Mag = Vector R
type T1_s = Double
type T2_s = Double
type Time_s = Double
type Freq_Hz = Double
type Angle_rad = Double
type Axis = [Double]
The heart of the simulation is the state monad where the state (NonEmpy BlochS
) is defined as a non-empty list of tuples of a time point and a 3d magnetization vector (BlochS
). The state transformer (BlochST
) accumulates (Bloch) tuples by consing to the head of the non-empty list. An MR pulse sequence is defined as a BlochST
.
type BlochS = (Time_s, Mag)
type BlochST = State (NonEmpty BlochS) ()
State transformation
To conveniently use the defined state monad BlochST
, we define the following helper functions:
initBlochS :: Time_s -> [Double] -> BlochS
= (t, fromList es)
initBlochS t es
updateBlochST :: (BlochS -> BlochS) -> BlochST
= do
updateBlochST f :| _) <- get
(s
modify (Lne.cons (f s))
runBlochST :: BlochST -> BlochS -> ([Time_s], [Mag])
=
runBlochST st s0 unzip $ (s0 :) $ Lne.toList $ Lne.reverse $ execState st (s0 :| [])
Once a pulse sequence is defined as a BlochST
, runBlochST
applies it to an initial BlochS
and restructures the result in a time vs. magnetization format easier for later visulatization.
Pulse sequence blocks
Now we can define MR pulse sequences. The two basic building blocks are:
- instant pulses and
- the free induction decay (FID) over a specified time period
blockST :: Axis -> Angle_rad -> Freq_Hz -> T1_s -> T2_s -> Time_s -> Int -> BlochST
= do
blockST ax th freq t1 t2 dt n
instPulseST ax th
fidST dt freq t1 t2 n
instPulseST :: Axis -> Angle_rad -> BlochST
= updateBlochST (second $ instPulseM (fromList ax) th)
instPulseST ax th
instPulseM :: Vector R -> Angle_rad -> Mag -> Mag
= rot u th #> m
instPulseM u th m
fidST :: Time_s -> Freq_Hz -> T1_s -> T2_s -> Int -> BlochST
= do
fidST dt freq t1 t2 n let (precs, decay, b) = freeprecess dt freq t1 t2
= precs <> decay
a $ updateBlochST (bimap (+ dt) (\m -> a #> m + b))
replicateM_ n
fidM :: Time_s -> Freq_Hz -> T1_s -> T2_s -> Mag -> Mag
=
fidM dt freq t1 t2 m let (precs, decay, b) = freeprecess dt freq t1 t2
= precs <> decay
a in a #> m + b
freeprecessM :: Time_s -> Freq_Hz -> T1_s -> T2_s -> Mag -> Mag
= (precs <> decay) #> m + recov
freeprecessM dt freq t1 t2 m where (precs, decay, recov) = freeprecess dt freq t1 t2
freeprecess :: Time_s -> Freq_Hz -> T1_s -> T2_s -> (Matrix R, Matrix R, Vector R)
= (precs, decay, recov)
freeprecess dt freq t1 t2 where
= exp $ -dt / t1
e1 = exp $ -dt / t2
e2 = diagl [e2, e2, e1]
decay = precess dt [0, 0, 1] freq
precs = fromList [0, 0, 1 - e1]
recov
precess :: Time_s -> Axis -> Freq_Hz -> Matrix R
= rot (fromList ax) (2 * pi * freq * dt)
precess dt ax freq
rot :: Vector R -> Angle_rad -> Matrix R
= scale c (ident 3) + scale (sin th) (crossProdMat u) + scale
rot u th 1 - c)
(
(outer u u)where c = cos th
crossProdMat :: Vector R -> Matrix R
= fromColumns $ map (cross v) (toColumns $ ident 3) crossProdMat v
TODO Plotting functionality using vega-lite
via hvega
plotComponents :: FilePath -> [PropertySpec] -> ([Time_s], [Mag]) -> IO ()
= do
plotComponents fname props xs let vega = toVegaLite $ vlDataToPropSpecs (toVLData xs) ++ props
$ fromVL vega
encodeFile fname
toVLData :: ([Time_s], [Mag]) -> Data
=
toVLData (ts, ms)
dataFromColumns []. dataColumn "time" (Numbers ts)
. dataColumn "M_x" (Numbers mx)
. dataColumn "M_y" (Numbers my)
. dataColumn "M_z" (Numbers mz)
$ []
where
= map (! 0) ms
mx = map (! 1) ms
my = map (! 2) ms
mz
vlDataToPropSpecs :: Data -> [PropertySpec]
=
vlDataToPropSpecs dat
[ dat
, trans []Line [MTooltip TTEncoding, MClip True]
, mark
, enc []800
, width 450
, height
]where
= transform . fold ["M_x", "M_y", "M_z"]
trans =
enc
encoding. position
X
PName "time"
[ PTitle "Time [s]"
, PmType Quantitative
, -- , PScale [SDomain (DNumbers [0, 1.0])]
]. position
Y
PName "value", PTitle "Magnetization [a.u.]", PmType Quantitative]
[. color [MName "key", MTitle "Component"]
Pulse sequences
Free induction decay
main_fid :: IO ()
= do
main_fid let dt = 1e-3
= [1, 0, 0]
m0 = 10
freq = 600e-3
t1 = 100e-3
t2 = 1000
n = runBlochST (fidST dt freq t1 t2 n) (initBlochS 0 m0)
res "fid.vg.json" [title "Free induction decay" []] res plotComponents
Saturation recovery
main_satrecov :: IO ()
= do
main_satrecov let nTR = 10
= [0, 1, 0]
ey = pi / 3
flipAngle = 0
freq = 600e-3
t1 = 100e-3
t2 = 1e-3
dt = 500
nFid = [0, 0, 1]
m0 = runBlochST
res $ blockST ey flipAngle freq t1 t2 dt nFid)
(replicateM_ nTR 0 m0)
(initBlochS "satrecov.vg.json" [title "Saturation recovery" []] res plotComponents
Spin echo
main_se :: IO ()
= do
main_se let ey = [0, 1, 0]
= [1, 0, 0]
ex = 10
freq = 600e-3
t1 = 100e-3
t2 = 1e-3
dt = 50e-3
te = 500e-3
tR = 1
nTR = [0, 0, 1]
m0 = runBlochST
res do
($ do
replicateM_ nTR pi / 2) freq t1 t2 dt (floor $ te / 2 / dt)
blockST ey (pi freq t1 t2 dt (floor $ (tR - te / 2) / dt)
blockST ex
)0 m0)
(initBlochS "se.vg.json" [title "Spin echo" []] res plotComponents
Gradient echo
tbc …
Spoiled gradient echo
tbc …
Script
To complete the script we need a main function:
main :: IO ()
= do
main
main_fid
main_satrecov main_se
Here is the complete code that can be run from the command line with stack
like
stack Bloch.hs
Intermittent Conclusion
This was a nice exercise in using the State Monad and trying out the hvega
. I will definitely try to learn more about vega(-lite) as it seems to be a great way to plot things, an important functionality which I wasn’t too happy with in Haskell yet. I might extend the code above to simulate an ensemble of isochromats and maybe use the project to practice some profiling.