factored out fulcrum into its own package
Getty Ritter
10 years ago
1 | module Data.Analysis.Fulcrum.Abstract | |
2 | ( Typeable | |
3 | , Abstract | |
4 | , makeAbs | |
5 | , absShow | |
6 | , absShowS | |
7 | ) where | |
8 | ||
9 | import Data.Dynamic | |
10 | import Data.Monoid | |
11 | import Data.Typeable (Typeable) | |
12 | ||
13 | data Abstract = Abstract | |
14 | { absVal :: Dynamic | |
15 | , absEq :: Abstract -> Bool | |
16 | , absCmp :: Abstract -> Ordering | |
17 | , absStr :: String | |
18 | } | |
19 | ||
20 | absShowS :: [Abstract] -> String | |
21 | absShowS [] = "" | |
22 | absShowS [x] = absShow x | |
23 | absShowS (x:xs) = absShow x ++ "," ++ absShowS xs | |
24 | ||
25 | absShow :: Abstract -> String | |
26 | absShow = absStr | |
27 | ||
28 | over :: Typeable a => (a -> b) -> b -> Dynamic -> b | |
29 | over op def dyn | |
30 | | Just x <- fromDynamic dyn = op x | |
31 | | otherwise = def | |
32 | ||
33 | -- | Oftentimes we want to compare two things without knowing that | |
34 | -- they're even the same type. We can wrap them in this and | |
35 | -- ensure that those operations are still possible. | |
36 | makeAbs :: (Typeable t, Ord t, Show t) => t -> Abstract | |
37 | makeAbs x = Abstract | |
38 | { absVal = toDyn x | |
39 | , absEq = doEq . absVal | |
40 | , absCmp = doCmp . absVal | |
41 | , absStr = show x | |
42 | } where doEq = over (== x) False | |
43 | doCmp y = over (compare x) (cmpTyp y) y | |
44 | cmpTyp y = dynTypeRep (toDyn x) `compare` dynTypeRep y | |
45 | ||
46 | instance Eq Abstract where a == b = a `absEq` b | |
47 | instance Show Abstract where show a = "Abstract(" ++ absStr a ++ ")" | |
48 | instance Ord Abstract where a `compare` b = a `absCmp` b |
1 | {-# LANGUAGE ExistentialQuantification #-} | |
2 | {-# LANGUAGE TemplateHaskell #-} | |
3 | {-# LANGUAGE RankNTypes #-} | |
4 | {-# LANGUAGE DeriveFunctor #-} | |
5 | {-# LANGUAGE RecordWildCards #-} | |
6 | {-# LANGUAGE ScopedTypeVariables #-} | |
7 | ||
8 | module Data.Analysis.Fulcrum.Analysis | |
9 | ( module Data.Analysis.Fulcrum.Abstract | |
10 | ||
11 | , runPlanToMap | |
12 | ||
13 | , Merge | |
14 | , Plan(..) | |
15 | , defaultPlan | |
16 | , Select | |
17 | , slName | |
18 | , Result | |
19 | ||
20 | , select | |
21 | , getUniqVals | |
22 | , getAxisValues | |
23 | , getLineValues | |
24 | , validRowValue | |
25 | ||
26 | , opBy | |
27 | , divBy | |
28 | ) where | |
29 | ||
30 | import Control.Applicative ((<$>)) | |
31 | import Control.Arrow (second) | |
32 | import Control.Lens | |
33 | import Data.Dynamic | |
34 | import Data.Maybe (catMaybes) | |
35 | import Data.Monoid (Monoid(..), (<>)) | |
36 | import Data.List (nub, sort, find) | |
37 | import Data.Map.Strict (Map, fromList) | |
38 | ||
39 | import Data.Analysis.Fulcrum.Abstract | |
40 | ||
41 | type AVal = [Abstract] | |
42 | ||
43 | -- | A merge function is anything that takes several | |
44 | -- rows and merges them to a single value. | |
45 | type Merge r = [r] -> r | |
46 | ||
47 | -- | A selector returns a list of abstract values, and is used | |
48 | -- for extracting from heterogeneous data. | |
49 | data Select r = Select | |
50 | { _slLens :: Getter r AVal | |
51 | , _slName :: String | |
52 | } | |
53 | ||
54 | slName :: Select r -> String | |
55 | slName = _slName | |
56 | ||
57 | -- | Turn a getter for a specific type into a getter of a generic | |
58 | -- value (so we can compare possibly across types) | |
59 | select :: (Typeable a, Ord a, Show a) => Getter r a -> String -> Select r | |
60 | select l s = Select (l.to go) s | |
61 | where go x = [makeAbs x] | |
62 | ||
63 | -- | A convenience function to combine strings with commas | |
64 | comma :: String -> String -> String | |
65 | comma x "" = x | |
66 | comma "" y = y | |
67 | comma x y = x <> "," <> y | |
68 | ||
69 | instance Monoid (Select r) where | |
70 | mappend (Select l ln) (Select r rn) = | |
71 | Select (to (\ x -> x^.l <> x^.r)) (comma ln rn) | |
72 | mempty = Select (to (const mempty)) "" | |
73 | ||
74 | -- | A convenience function to run a selector over data | |
75 | extract :: Select r -> r -> AVal | |
76 | extract (Select l _) r = (r^.l) | |
77 | ||
78 | -- | A plan describes how to go from a dataset of type [row] | |
79 | -- and arrive at a Result map | |
80 | data Plan row focus = Plan | |
81 | { planFocus :: Getter row focus -- ^ The final dependent variable | |
82 | , planFCName :: String -- ^ The name of the dependent variable | |
83 | , planName :: String -- ^ The name of the plan | |
84 | , planFilters :: row -> Bool -- ^ select which rows to keep | |
85 | , planMaps :: row -> row -- ^ transform them somehow | |
86 | , planAxis :: Select row -- ^ select an independent variable | |
87 | , planLines :: Select row -- ^ select sets of interesting data | |
88 | , planMerge :: Merge focus -- ^ join the resulting rows together | |
89 | , planLinAxis :: Bool -- ^ true if the axis is linear | |
90 | } | |
91 | ||
92 | defaultPlan :: Plan a a | |
93 | defaultPlan = Plan | |
94 | { planFocus = error "undefined focus" | |
95 | , planFCName = "undefined" | |
96 | , planName = "default plan" | |
97 | , planFilters = const True | |
98 | , planMaps = id | |
99 | , planAxis = mempty | |
100 | , planLines = mempty | |
101 | , planMerge = error "undefined merge" | |
102 | , planLinAxis = True | |
103 | } | |
104 | ||
105 | -- | The result of a plan is always going to be a map from | |
106 | -- (abstract but comparable) keys to some kind of result | |
107 | -- value. | |
108 | type Result r = Map (AVal, AVal) r | |
109 | ||
110 | -- | Given a selector and a list of rows, groups into sub-lists that | |
111 | -- share a common value for the selector. | |
112 | groupOn :: (Select r) -> [r] -> [[r]] | |
113 | groupOn l rs = [ filter (go v) rs | v <- getUniqVals l rs ] | |
114 | where go v r = extract l r == v | |
115 | ||
116 | -- | Extract every unique value that appears in the data | |
117 | getUniqVals :: (Select r) -> [r] -> [AVal] | |
118 | getUniqVals l rs = sort (nub (map (extract l) rs)) | |
119 | ||
120 | -- | Merge the focused value using the merge function | |
121 | combine :: Getter r f -> Merge f -> [r] -> f | |
122 | combine focus merge = merge . map (^.focus) | |
123 | ||
124 | -- | Group into a map based on the axis values and lines | |
125 | group :: Select r -> Select r -> [r] -> [((AVal, AVal), [r])] | |
126 | group l a rs = | |
127 | [ ((extract a (head r), extract l (head r)), r) | |
128 | | r <- groupOn (l <> a) rs | |
129 | ] | |
130 | ||
131 | -- | Execute a given plan on a set of data | |
132 | runPlanToMap :: Plan r f -> [r] -> Result f | |
133 | runPlanToMap p@(Plan { .. }) rawData = | |
134 | fromList $ map (second (combine planFocus planMerge)) | |
135 | $ group planLines planAxis | |
136 | $ map planMaps | |
137 | $ filter planFilters | |
138 | $ rawData | |
139 | ||
140 | -- | A convenience function for writing maps over rows | |
141 | opBy :: (a -> a -> a) -> Setter' r a -> Getter r a -> Getter r a -> r -> r | |
142 | opBy op t l r x = set t ((x^.l) `op` (x^.r)) x | |
143 | ||
144 | -- | A specialized convenience function for normalizing a given value | |
145 | -- by another by division | |
146 | divBy :: Lens' r Double -> Getter r Double -> r -> r | |
147 | divBy l = opBy (/) l l | |
148 | ||
149 | -- | A specialized convenience function for normalizing a given value | |
150 | -- by another using an nth root | |
151 | rootBy :: Lens' r Double -> Getter r Double -> r -> r | |
152 | rootBy l = undefined | |
153 | ||
154 | -- | Get the unique points to graph along the X axis | |
155 | getAxisValues :: Plan r f -> [r] -> [AVal] | |
156 | getAxisValues (Plan { .. }) rows = | |
157 | getUniqVals planAxis (filter planFilters rows) | |
158 | ||
159 | -- | Get the human-readable name of the X axis variable | |
160 | getAxisName :: Plan r f -> String | |
161 | getAxisName = slName . planAxis | |
162 | ||
163 | -- | Get the unique field values which determine the set of lines to produce | |
164 | getLineValues :: Plan r f -> [r] -> [AVal] | |
165 | getLineValues (Plan { .. }) rows = | |
166 | getUniqVals planLines (filter planFilters rows) | |
167 | ||
168 | -- | Get the human-readable name of the lines | |
169 | getLineName :: Plan r f -> String | |
170 | getLineName = slName . planLines | |
171 | ||
172 | -- | Determine whether a given (abstract) value appears in the data set | |
173 | validRowValue :: Select r -> [r] -> AVal -> Bool | |
174 | validRowValue l rs a = any (== a) (getUniqVals l rs) |
1 | {-# LANGUAGE RecordWildCards #-} | |
2 | {-# LANGUAGE ScopedTypeVariables #-} | |
3 | {-# LANGUAGE ParallelListComp #-} | |
4 | ||
5 | module Data.Analysis.Fulcrum.Plot (doPlot, doLogPlot) where | |
6 | ||
7 | import Control.Lens ((^.), (.~), (&), set, Lens', to) | |
8 | import Data.Colour (opaque, Colour) | |
9 | import Data.Colour.Names | |
10 | import Data.Default.Class (def) | |
11 | import Data.Map.Strict ((!), member) | |
12 | import Data.Maybe (fromJust) | |
13 | import Data.Monoid ((<>)) | |
14 | import Graphics.Rendering.Chart | |
15 | import Math.Statistics (stddev, average) | |
16 | ||
17 | import Data.Analysis.Fulcrum.Analysis | |
18 | ||
19 | type AVal = [Abstract] | |
20 | ||
21 | colors :: [Colour Double] | |
22 | colors = cycle [ blue, red, green, orange, purple, yellow ] | |
23 | ||
24 | plotLayout :: PlotValue b => Plan r f -> [r] -> Layout Int b | |
25 | plotLayout plan@(Plan { .. }) rows | |
26 | = layout_background .~ solidFillStyle (opaque white) | |
27 | $ layout_x_axis.laxis_generate .~ | |
28 | autoIndexAxis (map absShowS axisNames) | |
29 | $ layout_y_axis.laxis_title .~ planFCName | |
30 | $ layout_x_axis.laxis_title .~ slName planAxis | |
31 | $ layout_title .~ planName | |
32 | $ setLayoutForeground (opaque black) | |
33 | $ def | |
34 | where axisNames = getAxisValues plan rows | |
35 | ||
36 | plotLines :: (RealFloat f, PlotValue g) => | |
37 | Bool -> (Result f) -> String -> [(Int, AVal)] | |
38 | -> AVal -> Colour Double -> (f -> g) -> [PlotLines Int g] | |
39 | plotLines True vals axisTitle axisNames ln c conv = | |
40 | [ plot_lines_style .~ lineStyle 4 c | |
41 | $ plot_lines_values .~ | |
42 | [ [ (idx, conv (vals ! (ax, ln))) | |
43 | | (idx, ax) <- axisNames | |
44 | , (ax, ln) `member` vals | |
45 | , not (isNaN (vals ! (ax, ln))) ] ] | |
46 | $ plot_lines_title .~ axisTitle | |
47 | $ def | |
48 | ] | |
49 | plotLines False _ _ _ _ _ _ = [] | |
50 | ||
51 | plotDots :: (RealFloat f, PlotValue g) => Result f -> [(Int, AVal)] -> AVal | |
52 | -> Colour Double -> (f -> g) -> PlotPoints Int g | |
53 | plotDots vals axisNames ln color conv | |
54 | = plot_points_style .~ filledCircles 5 (opaque color) | |
55 | $ plot_points_values .~ | |
56 | [ (idx, conv (vals ! (ax, ln))) | |
57 | | (idx, ax) <- axisNames | |
58 | , (ax, ln) `member` vals | |
59 | , not (isNaN (vals ! (ax, ln))) ] | |
60 | $ def | |
61 | ||
62 | plotStds :: (RealFloat f) => Result f -> Result f -> | |
63 | [(Int, AVal)] -> AVal -> Colour Double -> PlotErrBars Int f | |
64 | plotStds avgs stds axisNames ln color | |
65 | = plot_errbars_line_style .~ lineStyle 1 color | |
66 | $ plot_errbars_values .~ | |
67 | [ symErrPoint idx (avgs ! (ax, ln)) 0 (stds ! (ax, ln)) | |
68 | | (idx, ax) <- axisNames | |
69 | , (ax, ln) `member` avgs | |
70 | , not (isNaN (stds ! (ax, ln))) | |
71 | , not (isNaN (avgs ! (ax, ln))) ] | |
72 | $ def | |
73 | ||
74 | doLogPlot :: Plan r Double -> [r] -> Renderable () | |
75 | doLogPlot plan@(Plan { .. }) rows = | |
76 | toRenderable $ layout_plots .~ map toPlot (concat plots) <> | |
77 | map toPlot dots | |
78 | $ plotLayout plan rows | |
79 | where plots = [ plotLines planLinAxis avgs "" axisNums ln c LogValue | |
80 | | ln <- lineNames | |
81 | | c <- colors | |
82 | ] | |
83 | dots = [ plotDots avgs axisNums ln c LogValue | |
84 | | ln <- lineNames | |
85 | | c <- colors | |
86 | ] | |
87 | lineNames = getLineValues plan rows | |
88 | axisNames = getAxisValues plan rows | |
89 | runPlan m = runPlanToMap (plan { planMerge = m }) | |
90 | avgs = runPlan average rows | |
91 | axisNums = zip [(0::Int)..] axisNames | |
92 | ||
93 | doPlot :: (RealFloat f, PlotValue f) => Plan r f -> [r] -> Renderable () | |
94 | doPlot plan@(Plan { .. }) rows = | |
95 | toRenderable $ layout_plots .~ map toPlot bars <> | |
96 | map toPlot (concat plots) <> | |
97 | map toPlot dots | |
98 | $ plotLayout plan rows | |
99 | where plots = [ plotLines planLinAxis avgs "" axisNums ln c id | |
100 | | ln <- lineNames | |
101 | | c <- colors | |
102 | ] | |
103 | dots = [ plotDots avgs axisNums ln c id | |
104 | | ln <- lineNames | |
105 | | c <- colors | |
106 | ] | |
107 | bars = [ plotStds avgs stds axisNums ln c | |
108 | | ln <- lineNames | |
109 | | c <- colors | |
110 | ] | |
111 | lineNames = getLineValues plan rows | |
112 | axisNames = getAxisValues plan rows | |
113 | runPlan m = runPlanToMap (plan { planMerge = m }) | |
114 | avgs = runPlan average rows | |
115 | stds = runPlan stddev rows | |
116 | axisNums = zip [(0::Int)..] axisNames | |
117 | ||
118 | lineStyle :: Double -> Colour Double -> LineStyle | |
119 | lineStyle n c | |
120 | = line_width .~ n | |
121 | $ line_color .~ opaque c | |
122 | $ def |
1 | module Data.Analysis.Fulcrum.Pretty (showResults) where | |
2 | ||
3 | import Data.List (nub) | |
4 | import Data.Map.Strict (Map) | |
5 | import qualified Data.Map.Strict as M | |
6 | ||
7 | import Data.Analysis.Fulcrum.Analysis | |
8 | ||
9 | showResults :: Show a => Result a -> String | |
10 | showResults = showTable . tabulate | |
11 | ||
12 | tabulate :: Show a => Result a -> [[String]] | |
13 | tabulate res = | |
14 | [ [""] ++ map absShowS cols ] ++ | |
15 | [ absShowS r : | |
16 | [ maybe "" show (M.lookup (c,r) res) | |
17 | | c <- cols | |
18 | ] | |
19 | | r <- rows | |
20 | ] | |
21 | where rows = nub $ map snd (M.keys res) | |
22 | cols = nub $ map fst (M.keys res) | |
23 | ||
24 | strJoin :: String -> [String] -> String | |
25 | strJoin c [] = [] | |
26 | strJoin c (x:[]) = x | |
27 | strJoin c (x:xs) = x ++ c ++ strJoin c xs | |
28 | ||
29 | rowJoin :: [String] -> String | |
30 | rowJoin = strJoin " | " | |
31 | ||
32 | showTable :: [[String]] -> String | |
33 | showTable tab = unlines $ map rowJoin $ map padRow tab | |
34 | where sizes :: [Int] | |
35 | sizes = [ maximum (map (length . (!! n)) tab) | |
36 | | n <- [0..(length (head tab))-1] | |
37 | ] | |
38 | pad len str = | |
39 | let diff = len - length str | |
40 | in str ++ (take diff $ repeat ' ') | |
41 | padRow :: [String] -> [String] | |
42 | padRow = zipWith pad sizes |
1 | module Data.Analysis.Fulcrum | |
2 | ( module Data.Analysis.Fulcrum.Analysis | |
3 | , module Data.Analysis.Fulcrum.Pretty | |
4 | ) where | |
5 | ||
6 | import Data.Analysis.Fulcrum.Analysis | |
7 | import Data.Analysis.Fulcrum.Pretty |
1 | name: fulcrum | |
2 | version: 0.1.0.1 | |
3 | license-file: LICENSE | |
4 | author: Getty Ritter | |
5 | maintainer: gdritter@galois.com | |
6 | build-type: Simple | |
7 | cabal-version: >=1.10 | |
8 | ||
9 | library | |
10 | exposed-modules: Data.Analysis.Fulcrum, | |
11 | Data.Analysis.Fulcrum.Abstract, | |
12 | Data.Analysis.Fulcrum.Analysis, | |
13 | Data.Analysis.Fulcrum.Plot | |
14 | Data.Analysis.Fulcrum.Pretty | |
15 | other-extensions: TemplateHaskell, | |
16 | RankNTypes, | |
17 | ExistentialQuantification, | |
18 | DeriveFunctor, | |
19 | RecordWildCards, | |
20 | ScopedTypeVariables, | |
21 | ParallelListComp | |
22 | build-depends: base >=4.6 && <4.7, | |
23 | lens >=4.1 && <4.2, | |
24 | containers >=0.5 && <0.6, | |
25 | Chart >=1.2 && <1.3, | |
26 | Chart-cairo >=1.2 && <1.3, | |
27 | colour >=2.3 && <2.4, | |
28 | data-default-class >=0.0 && <0.1, | |
29 | hstats >=0.3 && <0.4 | |
30 | default-language: Haskell2010 |