2 Module : Gargantext.Core.Viz.Graph.Tools
3 Description : Tools to build Graph
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
12 {-# LANGUAGE BangPatterns, ScopedTypeVariables #-}
14 module Gargantext.Core.Viz.Graph.Tools
18 import Data.HashMap.Strict (HashMap)
19 import Data.Map.Strict (Map)
20 import Data.Maybe (fromMaybe)
21 import Data.Swagger hiding (items)
22 import GHC.Float (sin, cos)
23 import GHC.Generics (Generic)
24 import Gargantext.API.Ngrams.Types (NgramsTerm(..))
25 import Gargantext.Core.Methods.Similarities (Similarity(..), measure)
26 -- import Gargantext.Core.Methods.Similarities.Conditional (conditional)
27 import Gargantext.Core.Statistics
28 import Gargantext.Core.Viz.Graph.Bridgeness (bridgeness, Bridgeness(..), Partitions, nodeId2comId{-, recursiveClustering-}, recursiveClustering', setNodes2clusterNodes)
29 import Gargantext.Core.Viz.Graph.Index (createIndices, toIndex, map2mat, mat2map, Index, MatrixShape(..))
30 import Gargantext.Core.Viz.Graph.Tools.IGraph (mkGraphUfromEdges, spinglass, spinglass')
31 import Gargantext.Core.Viz.Graph.Tools.Infomap (infomap)
32 import Gargantext.Core.Viz.Graph.Types (Attributes(..), Edge(..), Graph(..), MultiPartite(..), Node(..), Partite(..), Strength(..))
33 import Gargantext.Database.Schema.Ngrams (NgramsType(..))
34 import Gargantext.Core.Viz.Graph.Utils (edgesFilter, nodesFilter)
35 import Gargantext.Prelude
36 import Graph.Types (ClusterNode)
37 import IGraph.Random -- (Gen(..))
38 import Test.QuickCheck (elements)
39 import Test.QuickCheck.Arbitrary
40 import qualified Data.HashMap.Strict as HashMap
41 import qualified Data.List as List
42 import qualified Data.Map.Strict as Map
43 import qualified Data.Set as Set
44 import qualified Data.HashSet as HashSet
45 import qualified Data.Text as Text
46 import qualified Data.Vector.Storable as Vec
47 import qualified Graph.BAC.ProxemyOptim as BAC
48 import qualified IGraph as Igraph
49 import qualified IGraph.Algorithms.Layout as Layout
51 data PartitionMethod = Spinglass | Confluence | Infomap
52 deriving (Generic, Eq, Ord, Enum, Bounded, Show)
53 instance FromJSON PartitionMethod
54 instance ToJSON PartitionMethod
55 instance ToSchema PartitionMethod
56 instance Arbitrary PartitionMethod where
57 arbitrary = elements [ minBound .. maxBound ]
59 data BridgenessMethod = BridgenessMethod_Basic | BridgenessMethod_Advanced
60 deriving (Generic, Eq, Ord, Enum, Bounded, Show)
61 instance FromJSON BridgenessMethod
62 instance ToJSON BridgenessMethod
63 instance ToSchema BridgenessMethod
64 instance Arbitrary BridgenessMethod where
65 arbitrary = elements [ minBound .. maxBound ]
68 -------------------------------------------------------------
69 defaultClustering :: Map (Int, Int) Double -> IO [ClusterNode]
70 -- defaultClustering x = pure $ BAC.defaultClustering x
71 defaultClustering x = spinglass 1 x
73 -------------------------------------------------------------
74 type Threshold = Double
77 cooc2graph' :: Ord t => Similarity
80 -> Map (Index, Index) Double
81 cooc2graph' distance threshold myCooc
82 = Map.filter (> threshold)
86 Conditional -> map2mat Triangle 0 tiSize
87 Distributional -> map2mat Square 0 tiSize
88 $ Map.filter (> 1) myCooc'
91 (ti, _) = createIndices myCooc
93 myCooc' = toIndex ti myCooc
97 -- coocurrences graph computation
98 cooc2graphWith :: PartitionMethod
104 -> HashMap (NgramsTerm, NgramsTerm) Int
106 cooc2graphWith Spinglass = cooc2graphWith' (spinglass 1)
107 cooc2graphWith Confluence= cooc2graphWith' (\x -> pure $ BAC.defaultClustering x)
108 cooc2graphWith Infomap = cooc2graphWith' (infomap "-v -N2")
109 --cooc2graphWith Infomap = cooc2graphWith' (infomap "--silent --two-level -N2")
110 -- TODO: change these options, or make them configurable in UI?
112 cooc2graphWith' :: Partitions
118 -> HashMap (NgramsTerm, NgramsTerm) Int
120 cooc2graphWith' _doPartitions _bridgenessMethod multi similarity threshold strength myCooc = do
121 let (distanceMap, diag, ti) = doSimilarityMap similarity threshold strength myCooc
122 distanceMap `seq` diag `seq` ti `seq` return ()
124 partitions <- if (Map.size distanceMap > 0)
125 then recursiveClustering' (spinglass' 1) distanceMap
126 else panic $ Text.unlines [ "[Gargantext.C.V.Graph.Tools] Similarity Matrix is empty"
127 , "Maybe you should add more Map Terms in your list"
130 length partitions `seq` return ()
133 !confluence' = BAC.computeConfluences 3 (Map.keys distanceMap) True
134 !bridgeness' = bridgeness (Bridgeness_Recursive partitions 1.0 similarity) distanceMap
136 pure $ data2graph multi ti diag bridgeness' confluence' (setNodes2clusterNodes $ List.concat partitions)
142 doSimilarityMap :: Similarity
145 -> HashMap (NgramsTerm, NgramsTerm) Int
146 -> ( Map (Int,Int) Double
147 , Map (Index, Index) Int
148 , Map NgramsTerm Index
151 doSimilarityMap Conditional threshold strength myCooc = (distanceMap, toIndex ti myCooc', ti)
153 myCooc' = Map.fromList $ HashMap.toList myCooc
155 (_diag, theMatrix) = Map.partitionWithKey (\(x,y) _ -> x == y)
157 $ HashMap.toList myCooc
159 (ti, _it) = createIndices theMatrix
162 similarities = (\m -> m `seq` m)
163 $ (\m -> m `seq` measure Conditional m)
164 $ (\m -> m `seq` map2mat Square 0 tiSize m)
165 $ theMatrix `seq` toIndex ti theMatrix
167 links = round (let n :: Double = fromIntegral (Map.size ti) in 10 * n * (log n)^(2::Int))
168 distanceMap = Map.fromList
170 $ (if strength == Weak then List.reverse else identity)
173 $ Map.filter (> threshold)
174 -- $ conditional myCooc
175 $ similarities `seq` mat2map similarities
177 doSimilarityMap Distributional threshold strength myCooc = (distanceMap, toIndex ti diag, ti)
180 (diag, theMatrix) = Map.partitionWithKey (\(x,y) _ -> x == y)
182 $ HashMap.toList myCooc
184 (ti, _it) = createIndices theMatrix
187 similarities = (\m -> m `seq` m)
188 $ (\m -> m `seq` measure Distributional m)
189 $ (\m -> m `seq` map2mat Square 0 tiSize m)
190 $ theMatrix `seq` toIndex ti theMatrix
192 links = round (let n :: Double = fromIntegral tiSize in n * (log n)^(2::Int))
194 distanceMap = Map.fromList
196 $ (if strength == Weak then List.reverse else identity)
200 $ (\m -> m `seq` Map.filter (> threshold) m)
201 $ similarities `seq` mat2map similarities
203 ----------------------------------------------------------
204 -- | From data to Graph
205 type Occurrences = Int
207 nodeTypeWith :: MultiPartite -> NgramsTerm -> NgramsType
208 nodeTypeWith (MultiPartite (Partite s1 t1) (Partite _s2 t2)) t =
209 if HashSet.member t s1
213 data2graph :: MultiPartite
214 -> Map NgramsTerm Int
215 -> Map (Int, Int) Occurrences
216 -> Map (Int, Int) Double
217 -> Map (Int, Int) Double
220 data2graph multi labels' occurences bridge conf partitions =
221 Graph { _graph_nodes = nodes
222 , _graph_edges = edges
223 , _graph_metadata = Nothing
228 nodes = map (setCoord ForceAtlas labels bridge)
229 [ (n, Node { node_size = maybe 0 identity (Map.lookup (n,n) occurences)
230 , node_type = nodeTypeWith multi label
231 , node_id = (cs . show) n
232 , node_label = unNgramsTerm label
236 Attributes { clust_default = fromMaybe 0
237 (Map.lookup n community_id_by_node_id)
242 | (label, n) <- labels
243 , Set.member n toKeep
246 (bridge', toKeep) = nodesFilter (\v -> v > 1) bridge
248 edges = [ Edge { edge_source = cs (show s)
249 , edge_hidden = Nothing
250 , edge_target = cs (show t)
251 , edge_weight = weight
252 , edge_confluence = maybe 0 identity $ Map.lookup (s,t) conf
253 , edge_id = cs (show i)
255 | (i, ((s,t), weight)) <- zip ([0..]::[Integer] ) $ Map.toList bridge'
260 community_id_by_node_id = Map.fromList
261 $ map nodeId2comId partitions
263 labels = Map.toList labels'
266 ------------------------------------------------------------------------
268 data Layout = KamadaKawai | ACP | ForceAtlas
271 setCoord' :: (Int -> (Double, Double)) -> (Int, Node) -> Node
272 setCoord' f (i,n) = n { node_x_coord = x, node_y_coord = y }
278 setCoord :: Ord a => Layout -> [(a, Int)] -> Map (Int, Int) Double -> (Int, Node) -> Node
279 setCoord l labels m (n,node) = node { node_x_coord = x
283 (x,y) = getCoord l labels m n
289 -> Map (Int, Int) Double
292 getCoord KamadaKawai _ _m _n = undefined -- layout m n
294 getCoord ForceAtlas _ _ n = (sin d, cos d)
298 getCoord ACP labels m n = to2d $ maybe (panic "Graph.Tools no coordinate") identity
300 $ pcaReduceTo (Dimension 2)
303 to2d :: Vec.Vector Double -> (Double, Double)
306 ds = take 2 $ Vec.toList v
310 mapArray :: Ord a => [(a, Int)] -> Map (Int, Int) Double -> Map Int (Vec.Vector Double)
311 mapArray items m' = Map.fromList [ toVec n' ns m' | n' <- ns ]
315 toVec :: Int -> [Int] -> Map (Int,Int) Double -> (Int, Vec.Vector Double)
316 toVec n' ns' m'' = (n', Vec.fromList $ map (\n'' -> maybe 0 identity $ Map.lookup (n',n'') m'') ns')
317 ------------------------------------------------------------------------
319 -- | KamadaKawai Layout
320 -- TODO TEST: check labels, nodeId and coordinates
321 layout :: Map (Int, Int) Double -> Int -> Gen -> (Double, Double)
322 layout m n gen = maybe (panic "") identity $ Map.lookup n $ coord
324 coord :: (Map Int (Double,Double))
325 coord = Map.fromList $ List.zip (Igraph.nodes g) $ (Layout.layout g p gen)
326 --p = Layout.defaultLGL
327 p = Layout.kamadaKawai
328 g = mkGraphUfromEdges $ map fst $ List.filter (\e -> snd e > 0) $ Map.toList m
330 -----------------------------------------------------------------------------
332 cooc2graph'' :: Ord t => Similarity
335 -> Map (Index, Index) Double
336 cooc2graph'' distance threshold myCooc = neighbourMap
338 (ti, _) = createIndices myCooc
339 myCooc' = toIndex ti myCooc
340 matCooc = map2mat Triangle 0 (Map.size ti) $ Map.filter (> 1) myCooc'
341 distanceMat = measure distance matCooc
342 neighbourMap = filterByNeighbours threshold
343 $ mat2map distanceMat
346 filterByNeighbours :: Double -> Map (Index, Index) Double -> Map (Index, Index) Double
347 filterByNeighbours threshold distanceMap = filteredMap
350 indexes = List.nub $ List.concat $ map (\(idx,idx') -> [idx,idx'] ) $ Map.keys distanceMap
351 filteredMap :: Map (Index, Index) Double
352 filteredMap = Map.fromList
355 let selected = List.reverse
359 $ Map.filterWithKey (\(from,_) _ -> idx == from) distanceMap
360 in List.take (round threshold) selected