diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for digraph
+
+## 0.1.0.0 -- 2019-05-30
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2019, Kadena LLC
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Lars Kuhtz nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+[![Build Status](https://travis-ci.org/kadena-io/digraph.svg?branch=master)](https://travis-ci.org/kadena-io/digraph)
+
+Directed graphs in adjacency set representation. The implementation is based
+on `Data.HashMap.Strict` and `Data.HashSet` from the [unordered-containers
+package](https://hackage.haskell.org/package/unordered-containers).
+
+Undirected graphs are represented as symmetric, irreflexive directed graphs.
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/digraph.cabal b/digraph.cabal
new file mode 100644
--- /dev/null
+++ b/digraph.cabal
@@ -0,0 +1,68 @@
+cabal-version: 2.2
+name: digraph
+version: 0.1.0.0
+synopsis: Directed Graphs
+description: Directed graphs implementation that is based on unordered-containers
+homepage: https://github.com/kadena-io/digraph
+bug-reports: https://github.com/kadena-io/digraph/issues
+license: BSD-3-Clause
+license-file: LICENSE
+author: Lars Kuhtz
+maintainer: lars@kadena.io
+copyright: Copyright (c) 2019, Kadena LLC
+category: Data, Mathematics
+tested-with:
+      GHC==8.6.5
+    , GHC==8.4.4
+    , GHC==8.2.2
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+    type: git
+    location: https://github.com/kadena-io/digraph.git
+
+library
+    hs-source-dirs: src
+    default-language: Haskell2010
+    ghc-options:
+        -Wall
+    exposed-modules:
+          Data.DiGraph
+        , Data.DiGraph.FloydWarshall
+        , Data.DiGraph.Random
+    build-depends:
+          base >=4.10 && <4.14
+        , containers >=0.5
+        , deepseq >=1.4
+        , hashable >=1.3
+        , massiv >=0.3
+        , mwc-random >=0.14
+        , streaming >=0.2
+        , transformers >=0.5
+        , unordered-containers >=0.2
+
+test-suite digraph-tests
+    type: exitcode-stdio-1.0
+    hs-source-dirs: test
+    default-language: Haskell2010
+    main-is: Main.hs
+    ghc-options:
+        -Wall
+        -threaded
+        -with-rtsopts=-N
+    other-modules:
+          Data.DiGraph.Test
+        , Data.DiGraph.Random.Test
+    build-depends:
+        -- internal
+          digraph
+
+        -- external
+        , QuickCheck >=2.11
+        , base >=4.10 && <4.14
+        , fgl >=5.7
+        , hashable >=1.3
+        , massiv >=0.3
+
diff --git a/src/Data/DiGraph.hs b/src/Data/DiGraph.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/DiGraph.hs
@@ -0,0 +1,605 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Module: DiGraph
+-- Copyright: Copyright © 2018-2019 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+-- Directed graphs in adjacency set representation. The implementation is based
+-- on "Data.HashMap.Strict" and "Data.HashSet" from the [unordered-containers
+-- package](https://hackage.haskell.org/package/unordered-containers)
+--
+-- Undirected graphs are represented as symmetric, irreflexive directed graphs.
+--
+module Data.DiGraph
+( DiGraph
+, DiEdge
+, adjacencySets
+, vertices
+, edges
+, adjacents
+, incidents
+
+-- * Construction and Modification of Graphs
+, insertEdge
+, fromEdges
+, insertVertex
+, mapVertices
+, union
+, transpose
+, symmetric
+, fromList
+, unsafeFromList
+
+-- * Predicates
+, isDiGraph
+, isAdjacent
+, isRegular
+, isSymmetric
+, isIrreflexive
+, isEdge
+, isVertex
+
+-- * Properties
+, order
+, size
+, diSize
+, symSize
+, outDegree
+, inDegree
+, maxOutDegree
+, maxInDegree
+, minOutDegree
+, minInDegree
+
+-- * Distances, Shortest Paths, and Diameter
+, ShortestPathCache
+, shortestPathCache
+, shortestPath
+, shortestPath_
+, distance
+, distance_
+, diameter
+, diameter_
+
+-- * Graphs
+, emptyGraph
+, singleton
+, clique
+, pair
+, triangle
+, cycle
+, diCycle
+, line
+, diLine
+, petersonGraph
+, twentyChainGraph
+, hoffmanSingleton
+
+) where
+
+import Control.Arrow
+import Control.DeepSeq
+import Control.Monad
+
+import Data.Foldable
+import Data.Hashable (Hashable)
+import qualified Data.HashMap.Strict as HM
+import qualified Data.HashSet as HS
+import qualified Data.List as L
+import Data.Maybe
+import Data.Semigroup
+import Data.Traversable
+import Data.Tuple
+
+import GHC.Generics
+
+import Numeric.Natural
+
+import Prelude hiding (cycle)
+
+-- internal modules
+
+import qualified Data.DiGraph.FloydWarshall as FW
+
+-- -------------------------------------------------------------------------- --
+-- Utils
+
+int :: Integral a => Num b => a -> b
+int = fromIntegral
+{-# INLINE int #-}
+
+-- -------------------------------------------------------------------------- --
+-- Graph
+
+-- | Directed Edge.
+--
+type DiEdge a = (a, a)
+
+-- | Adjacency set representation of directed graphs.
+--
+-- It is assumed that each target of an edge is also explicitly a vertex in the
+-- graph.
+--
+-- It is not generally required that graphs are irreflexive, but all concrete
+-- graphs that are defined in this module are irreflexive.
+--
+-- Undirected graphs are represented as symmetric directed graphs.
+--
+newtype DiGraph a = DiGraph { unGraph :: HM.HashMap a (HS.HashSet a) }
+    deriving (Show, Eq, Ord, Generic)
+    deriving anyclass (NFData, Hashable)
+
+instance (Hashable a, Eq a) => Semigroup (DiGraph a) where
+    (DiGraph a) <> (DiGraph b) = DiGraph (HM.unionWith (<>) a b)
+    {-# INLINE (<>) #-}
+
+instance (Hashable a, Eq a) => Monoid (DiGraph a) where
+    mempty = DiGraph mempty
+    mappend = (<>)
+    {-# INLINE mempty #-}
+    {-# INLINE mappend #-}
+
+-- | A predicate that asserts that every target of an edge is also a vertex in
+-- the graph. Any graph that is constructed without using unsafe methods is
+-- guaranteed to satisfy this predicate.
+--
+isDiGraph :: Eq a => Hashable a => DiGraph a -> Bool
+isDiGraph g@(DiGraph m) = HS.null (HS.unions (HM.elems m) `HS.difference` vertices g)
+{-# INLINE isDiGraph #-}
+
+-- | The adjacency sets of a graph.
+--
+adjacencySets :: DiGraph a -> HM.HashMap a (HS.HashSet a)
+adjacencySets = unGraph
+{-# INLINE adjacencySets #-}
+
+-- | The set of vertices of the graph.
+--
+vertices :: DiGraph a -> HS.HashSet a
+vertices = HS.fromMap . HM.map (const ()) . unGraph
+{-# INLINE vertices #-}
+
+-- | The set edges of the graph.
+--
+edges :: Eq a => Hashable a => DiGraph a -> HS.HashSet (DiEdge a)
+edges = HS.fromList . concatMap (traverse HS.toList) . HM.toList . unGraph
+{-# INLINE edges #-}
+
+-- | The set of adjacent pairs of a graph.
+--
+adjacents :: Eq a => Hashable a => a -> DiGraph a -> HS.HashSet a
+adjacents a (DiGraph g) = g HM.! a
+{-# INLINE adjacents #-}
+
+-- | The set of incident edges of a graph.
+--
+incidents :: Eq a => Hashable a => a -> DiGraph a -> [(a, a)]
+incidents a g = [ (a, b) | b <- toList (adjacents a g) ]
+{-# INLINE incidents #-}
+
+-- -------------------------------------------------------------------------- --
+-- Constructing and Modifying Graphs
+
+-- | Construct a graph from adjacency lists.
+--
+fromList :: Eq a => Hashable a => [(a,[a])] -> DiGraph a
+fromList l = foldr insertVertex es (fst <$> l)
+  where
+    es = fromEdges [ (a,b) | (a, bs) <- l, b <- bs ]
+{-# INLINE fromList #-}
+
+-- | Unsafely construct a graph from adjacency lists.
+--
+-- This function assumes that the input includes a adjacency list of each vertex
+-- that appears in a adjacency list of another vertex. Generally, 'fromList'
+-- should be preferred.
+--
+unsafeFromList :: Eq a => Hashable a => [(a,[a])] -> DiGraph a
+unsafeFromList = DiGraph . HM.map HS.fromList . HM.fromList
+{-# INLINE unsafeFromList #-}
+
+-- | Construct a graph from a foldable structure of edges.
+--
+fromEdges :: Eq a => Hashable a => Foldable f => f (a, a) -> DiGraph a
+fromEdges = foldr insertEdge mempty
+{-# INLINE fromEdges #-}
+
+-- | The union of two graphs.
+--
+union :: Eq a => Hashable a => DiGraph a -> DiGraph a -> DiGraph a
+union = (<>)
+{-# INLINE union #-}
+
+-- | Map a function over all vertices of a graph.
+--
+mapVertices :: Eq b => Hashable b => (a -> b) -> DiGraph a -> DiGraph b
+mapVertices f = DiGraph . HM.fromList . fmap (f *** HS.map f) . HM.toList . unGraph
+{-# INLINE mapVertices #-}
+
+-- | Transpose a graph, i.e. reverse all edges of the graph.
+--
+transpose :: Eq a => Hashable a => DiGraph a -> DiGraph a
+transpose g = (DiGraph $ mempty <$ unGraph g)
+    `union` (fromEdges . HS.map swap $ edges g)
+
+-- | Symmetric closure of a graph.
+--
+symmetric :: Eq a => Hashable a => DiGraph a -> DiGraph a
+symmetric g = g <> transpose g
+{-# INLINE symmetric #-}
+
+-- | Insert an edge. Returns the graph unmodified if the edge is already in the
+-- graph. Non-existing vertices are added.
+--
+insertEdge :: Eq a => Hashable a => DiEdge a -> DiGraph a -> DiGraph a
+insertEdge (a,b) = DiGraph
+    . HM.insertWith (<>) a [b]
+    . HM.insertWith (<>) b []
+    . unGraph
+{-# INLINE insertEdge #-}
+
+-- | Insert a vertex. Returns the graph unmodified if the vertex is already in
+-- the graph.
+--
+insertVertex :: Eq a => Hashable a => a -> DiGraph a -> DiGraph a
+insertVertex a = DiGraph . HM.insertWith (<>) a [] . unGraph
+{-# INLINE insertVertex #-}
+
+-- -------------------------------------------------------------------------- --
+-- Properties
+
+-- | The order of a graph is the number of vertices.
+--
+order :: DiGraph a -> Natural
+order = int . HS.size . vertices
+{-# INLINE order #-}
+
+-- | Directed Size. This the number of edges of the graph.
+--
+diSize :: Eq a => Hashable a => DiGraph a -> Natural
+diSize = int . HS.size . edges
+{-# INLINE diSize #-}
+
+-- | Directed Size. This the number of edges of the graph.
+--
+size :: Eq a => Hashable a => DiGraph a -> Natural
+size = int . HS.size . edges
+{-# INLINE size #-}
+
+-- | Undirected Size of a graph. This is the number of edges of the symmetric
+-- closure of the graph.
+--
+symSize :: Eq a => Hashable a => DiGraph a -> Natural
+symSize g = diSize (symmetric g) `div` 2
+{-# INLINE symSize #-}
+
+-- | The number of outgoing edges of vertex in a graph.
+--
+outDegree :: Eq a => Hashable a => DiGraph a -> a -> Natural
+outDegree (DiGraph g) a = int . HS.size $ g HM.! a
+{-# INLINE outDegree #-}
+
+-- | The maximum out-degree of the vertices of a graph.
+--
+maxOutDegree :: Eq a => Hashable a => DiGraph a -> Natural
+maxOutDegree g = maximum $ HS.map (outDegree g) (vertices g)
+{-# INLINE maxOutDegree #-}
+
+-- | The minimum out-degree of the vertices of a graph.
+--
+minOutDegree :: Eq a => Hashable a => DiGraph a -> Natural
+minOutDegree g = minimum $ HS.map (outDegree g) (vertices g)
+{-# INLINE minOutDegree #-}
+
+-- | The number of incoming edges of vertex in a graph.
+--
+inDegree :: Eq a => Hashable a => DiGraph a -> a -> Natural
+inDegree g = outDegree (transpose g)
+{-# INLINE inDegree #-}
+
+-- | The maximum in-degree of the vertices of a graph.
+--
+maxInDegree :: Eq a => Hashable a => DiGraph a -> Natural
+maxInDegree = maxOutDegree . transpose
+{-# INLINE maxInDegree #-}
+
+-- | The minimum in-degree of the vertices of a graph.
+--
+minInDegree :: Eq a => Hashable a => DiGraph a -> Natural
+minInDegree = minOutDegree . transpose
+{-# INLINE minInDegree #-}
+
+-- -------------------------------------------------------------------------- --
+-- Predicates
+
+-- | Return whether a graph is regular, i.e. whether all vertices have the same
+-- out-degree. Note that the latter implies that all vertices also have the same
+-- in-degree.
+--
+isRegular :: DiGraph a -> Bool
+isRegular = (== 1)
+    . length
+    . L.group
+    . fmap (HS.size . snd)
+    . HM.toList
+    . unGraph
+{-# INLINE isRegular #-}
+
+-- | Return whether a graph is symmetric, i.e. whether for each edge \((a,b)\)
+-- there is also the edge \((b,a)\) in the graph.
+--
+isSymmetric :: Hashable a => Eq a => DiGraph a -> Bool
+isSymmetric g = all checkVertex $ HM.toList $ unGraph g
+  where
+    checkVertex (a, e) = all (\x -> isAdjacent x a g) e
+{-# INLINE isSymmetric #-}
+
+-- | Return whether a graph is irreflexive. A graph is irreflexive if for each
+-- edge \((a,b)\) it holds that \(a \neq b\), i.e there are no self-loops in the
+-- graph.
+--
+isIrreflexive :: Eq a => Hashable a => DiGraph a -> Bool
+isIrreflexive = not . any (uncurry HS.member) . HM.toList . unGraph
+{-# INLINE isIrreflexive #-}
+
+-- | Return whether a vertex is contained in a graph.
+--
+isVertex :: Eq a => Hashable a => a -> DiGraph a -> Bool
+isVertex a = HM.member a . unGraph
+{-# INLINE isVertex #-}
+
+-- | Return whether an edge is contained in a graph.
+--
+isEdge :: Eq a => Hashable a => DiEdge a -> DiGraph a -> Bool
+isEdge (a, b) = maybe False (HS.member b) . HM.lookup a . unGraph
+{-# INLINE isEdge #-}
+
+-- | Return whether two vertices are adjacent in a graph.
+--
+isAdjacent :: Eq a => Hashable a => a -> a -> DiGraph a -> Bool
+isAdjacent = curry isEdge
+{-# INLINE isAdjacent #-}
+
+-- -------------------------------------------------------------------------- --
+-- Distances, Shortest Paths, and Diameter
+
+-- | The shortest path matrix of a graph.
+--
+-- The shortest path matrix of a graph can be used to efficiently query the
+-- distance and shortest path between any two vertices of the graph. It can also
+-- be used to efficiently compute the diameter of the graph.
+--
+-- Computing the shortest path matrix is expensive for larger graphs. The matrix
+-- is computed using the Floyd-Warshall algorithm. The space and time complexity
+-- is quadratic in the /order/ of the graph. For sparse graphs there are more
+-- efficient algorithms for computing distances and shortest paths between the
+-- nodes of the graph.
+--
+data ShortestPathCache a = ShortestPathCache
+    {-# UNPACK #-} !FW.ShortestPathMatrix
+        -- ^ The shortest path matrix of a graph.
+    !(HM.HashMap a Int)
+        -- ^ mapping from vertices of the graph to indices in the shortest path
+        -- matrix.
+    !(HM.HashMap Int a)
+        -- ^ mapping from indices in the shortest path matrix to vertices in the
+        -- graph.
+    deriving (Show, Eq, Ord, Generic)
+    deriving anyclass (NFData)
+
+-- | Compute the shortest path matrix for a graph. The result can be used to
+-- efficiently query the distance and shortest path between any two vertices of
+-- the graph. It can also be used to efficiently compute the diameter of the
+-- graph.
+--
+shortestPathCache :: Eq a => Hashable a => DiGraph a -> ShortestPathCache a
+shortestPathCache g = ShortestPathCache m vmap rvmap
+  where
+    m = FW.floydWarshall $ FW.fromAdjacencySets (unGraph ig)
+    ig = mapVertices (vmap HM.!) g
+    vmap = HM.fromList $ zip (HS.toList $ vertices g) [0..]
+    rvmap = HM.fromList $ zip [0..] (HS.toList $ vertices g)
+
+-- | Compute the Diameter of a graph, i.e. the maximum length of a shortest path
+-- between two vertices in the graph.
+--
+-- This is expensive to compute for larger graphs. If also the shortest paths or
+-- distances are needed, one should use 'shortestPathCache' to cache the result
+-- of the search and use the 'diameter_', 'shortestPath_', and 'distance_' to
+-- query the respective results from the cache.
+--
+-- The algorithm is optimized for dense graphs. For large sparse graphs a more
+-- efficient algorithm should be used.
+--
+diameter :: Eq a => Hashable a => DiGraph a -> Maybe Natural
+diameter = diameter_ . shortestPathCache
+{-# INLINE diameter #-}
+
+-- | Compute the Diameter of a graph from a shortest path matrix. The diameter
+-- of a graph is the maximum length of a shortest path between two vertices in
+-- the graph.
+--
+diameter_ :: ShortestPathCache a -> Maybe Natural
+diameter_ (ShortestPathCache m _ _) = round <$> FW.diameter m
+{-# INLINE diameter_ #-}
+
+-- | Compute the shortest path between two vertices of a graph.
+--
+-- | This is expensive for larger graphs. If more than one path is needed one
+-- should use 'shortestPathCache' to cache the result of the search and use
+-- 'shortestPath_' to query paths from the cache.
+--
+-- The algorithm is optimized for dense graphs. For large sparse graphs a more
+-- efficient algorithm should be used.
+--
+shortestPath :: Eq a => Hashable a => a -> a -> DiGraph a -> Maybe [a]
+shortestPath src trg = shortestPath_ src trg . shortestPathCache
+{-# INLINE shortestPath #-}
+
+-- | Compute the shortest path between two vertices from the shortest path
+-- matrix of a graph.
+--
+-- The algorithm is optimized for dense graphs. For large sparse graphs a more
+-- efficient algorithm should be used.
+--
+shortestPath_ :: Eq a => Hashable a => a -> a -> ShortestPathCache a -> Maybe [a]
+shortestPath_ src trg (ShortestPathCache c m r)
+    = fmap ((HM.!) r) <$> FW.shortestPath c (m HM.! src) (m HM.! trg)
+{-# INLINE shortestPath_ #-}
+
+-- | Compute the distance between two vertices of a graph.
+--
+-- | This is expensive for larger graphs. If more than one distance is needed
+-- one should use 'shortestPathCache' to cache the result of the search and use
+-- 'distance_' to query paths from the cache.
+--
+-- The algorithm is optimized for dense graphs. For large sparse graphs a more
+-- efficient algorithm should be used.
+--
+distance :: Eq a => Hashable a => a -> a -> DiGraph a -> Maybe Natural
+distance src trg = distance_ src trg . shortestPathCache
+{-# INLINE distance #-}
+
+-- | Compute the distance between two vertices from the shortest path matrix of
+-- a graph.
+--
+-- The algorithm is optimized for dense graphs. For large sparse graphs a more
+-- efficient algorithm should be used.
+--
+distance_ :: Eq a => Hashable a => a -> a -> ShortestPathCache a -> Maybe Natural
+distance_ src trg (ShortestPathCache c m _)
+    = round <$> FW.distance c (m HM.! src) (m HM.! trg)
+{-# INLINE distance_ #-}
+
+-- -------------------------------------------------------------------------- --
+-- Concrete Graph
+
+-- | The empty graph on @n@ nodes. This is the graph of 'order' @n@ and 'size'
+-- @0@.
+--
+emptyGraph :: Natural -> DiGraph Int
+emptyGraph n = unsafeFromList [ (i, []) | i <- [0 .. int n - 1] ]
+
+-- | Undirected clique.
+--
+clique :: Natural -> DiGraph Int
+clique i = unsafeFromList
+    [ (a, b)
+    | a <- [0 .. int i - 1]
+    , let b = [ x | x <- [0 .. int i - 1] , x /= a ]
+    ]
+
+-- | The (irreflexive) singleton graph.
+--
+singleton :: DiGraph Int
+singleton = clique 1
+
+-- | Undirected pair.
+--
+pair :: DiGraph Int
+pair = clique 2
+
+-- | Undirected triangle.
+--
+triangle :: DiGraph Int
+triangle = clique 3
+
+-- | Directed cycle.
+--
+diCycle :: Natural -> DiGraph Int
+diCycle n = unsafeFromList [ (a, [(a + 1) `mod` int n]) | a <- [0 .. int n - 1] ]
+
+-- | Undirected cycle.
+--
+cycle :: Natural -> DiGraph Int
+cycle = symmetric . diCycle
+
+-- | Directed line.
+--
+diLine :: Natural -> DiGraph Int
+diLine n = unsafeFromList [ (a, [ a + 1 | a /= int n - 1]) | a <- [0 .. int n - 1] ]
+
+-- | Undirected line.
+--
+line :: Natural -> DiGraph Int
+line = symmetric . diLine
+
+-- | The Peterson graph.
+--
+petersonGraph :: DiGraph Int
+petersonGraph = DiGraph
+    [ (0, [2,3,5])
+    , (1, [3,4,6])
+    , (2, [4,0,7])
+    , (3, [0,1,8])
+    , (4, [1,2,9])
+    , (5, [0,6,9])
+    , (6, [1,5,7])
+    , (7, [2,6,8])
+    , (8, [3,7,9])
+    , (9, [4,8,5])
+    ]
+
+-- | The "twenty chain" graph.
+--
+twentyChainGraph :: DiGraph Int
+twentyChainGraph  = pentagram `union` pentagon1 `union` pentagon2 `union` connections
+  where
+    pentagram = mapVertices (+ 5) $ pentagon2pentagram $ cycle 5
+    pentagon1 = mapVertices (+ 10) $ cycle 5
+    pentagon2 = mapVertices (+ 15) $ cycle 5
+    connections = fromEdges $ HS.fromList $ mconcat
+        [ [(i, x), (x, i)]
+        | i <- [0..4]
+        , x <- [i + 5, i + 10, i + 15]
+        ]
+    pentagon2pentagram = mapVertices $ \case
+        0 -> 0
+        1 -> 3
+        2 -> 1
+        3 -> 4
+        4 -> 2
+        _ -> error "invalid vertex"
+
+-- | Hoffman-Singleton Graph.
+--
+-- The Hoffman-Singleton graph is a 7-regular graph with 50 vertices and 175
+-- edges. It's the largest graph of max-degree 7 and diameter 2. Cf.
+-- [https://en.wikipedia.org/wiki/Hoffman–Singleton_graph]()
+--
+hoffmanSingleton :: DiGraph Int
+hoffmanSingleton = pentagons `union` pentagrams `union` connections
+  where
+    pentagons = mconcat
+        [ mapVertices (p_off i) $ cycle 5 | i <- [0 .. 4] ]
+    pentagrams = mconcat
+        [ mapVertices (q_off i) $ pentagon2pentagram $ cycle 5 | i <- [0 .. 4] ]
+
+    p_off h = (+) (25 + 5 * h)
+    q_off i = (+) (5 * i)
+
+    pentagon2pentagram = mapVertices $ \case
+        0 -> 0
+        1 -> 3
+        2 -> 1
+        3 -> 4
+        4 -> 2
+        _ -> error "invalid vertex"
+
+    connections = fromEdges $ HS.fromList $ mconcat
+        [ [(a, b), (b, a)]
+        | h <- [0 .. 4]
+        , j <- [0 .. 4]
+        , let a = p_off h j
+        , i <- [0 .. 4]
+        , let b = q_off i ((h * i + j) `mod` 5)
+        ]
+
diff --git a/src/Data/DiGraph/FloydWarshall.hs b/src/Data/DiGraph/FloydWarshall.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/DiGraph/FloydWarshall.hs
@@ -0,0 +1,240 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE ExplicitNamespaces #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+-- |
+-- Module: Data.DiGraph.FloydWarshall
+-- Copyright: Copyright © 2018 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+-- TODO
+--
+module Data.DiGraph.FloydWarshall
+(
+-- * Graph Representations
+  DenseAdjMatrix
+, AdjacencySets
+
+-- * Conversions
+, fromAdjacencySets
+, toAdjacencySets
+
+-- * FloydWarshall Algorithm
+, ShortestPathMatrix(..)
+, floydWarshall
+, shortestPath
+, distance
+, diameter
+
+-- * Legacy exports
+, distMatrix_
+, floydWarshall_
+, diameter_
+, shortestPaths_
+) where
+
+import Control.DeepSeq
+
+import Data.Foldable
+import qualified Data.HashMap.Strict as HM
+import qualified Data.HashSet as HS
+import Data.Massiv.Array as M
+#if !MIN_VERSION_base(4,11,0)
+import Data.Semigroup
+#endif
+
+import GHC.Generics
+
+import Numeric.Natural
+
+-- -------------------------------------------------------------------------- --
+-- Graph Representations
+
+-- | Adjacency matrix representation of a directed graph.
+--
+type DenseAdjMatrix = Array U Ix2 Int
+
+-- | Adjacency set representation of a directed graph.
+--
+type AdjacencySets = HM.HashMap Int (HS.HashSet Int)
+
+-- -------------------------------------------------------------------------- --
+-- Adjacency Matrix for dense Graphs
+--
+-- (uses massiv)
+
+-- | Assumes that the input is an undirected graph and that the vertex
+-- set is a prefix of the natural numbers.
+--
+fromAdjacencySets :: AdjacencySets -> DenseAdjMatrix
+fromAdjacencySets g = makeArray Seq (Sz (n :. n)) go
+  where
+    n = HM.size g
+    go (i :. j)
+        | isEdge (i, j) = 1
+        | isEdge (j, i) = 1
+        | otherwise = 0
+    isEdge (a, b) = maybe False (HS.member b) $ HM.lookup a g
+
+-- | Converts an adjacency matrix into a graph in adjacnency set representation.
+--
+toAdjacencySets :: DenseAdjMatrix -> AdjacencySets
+toAdjacencySets = ifoldlS f mempty
+  where
+    f a (i :. j) x
+        | x == 0 = a
+        | otherwise = HM.insertWith (<>) i (HS.singleton j) a
+
+-- -------------------------------------------------------------------------- --
+-- Floyd Warshall with Paths
+
+-- | Shortest path matrix of a graph.
+--
+newtype ShortestPathMatrix = ShortestPathMatrix (Array U Ix2 (Double, Int))
+    deriving (Show, Eq, Ord, Generic)
+    deriving newtype (NFData)
+
+-- | Shortest path computation for integral matrixes.
+--
+floydWarshall :: Unbox a => Real a => Array U Ix2 a -> ShortestPathMatrix
+floydWarshall = ShortestPathMatrix
+    . floydWarshallInternal
+    . computeAs U
+    . intDistMatrix
+
+-- | Compute a shortest path between two vertices of a graph from the shortest
+-- path matrix of the graph.
+--
+shortestPath
+    :: ShortestPathMatrix
+    -> Int
+    -> Int
+    -> Maybe [Int]
+shortestPath (ShortestPathMatrix m) src trg
+    | M.isEmpty mat = Nothing
+    | not (M.isSafeIndex (size m) (src :. trg)) = Nothing
+    | (mat M.! (src :. trg)) == (-1) = Nothing
+    | otherwise = go src trg
+  where
+    mat = M.computeAs U $ M.map snd m
+    go a b
+        | a == b = return []
+        | otherwise = do
+            n <- M.index mat (a :. b)
+            (:) n <$> go n b
+
+-- | Compute the distance between two vertices of a graph from the shortest path
+-- matrix of the graph.
+--
+distance :: ShortestPathMatrix -> Int -> Int -> Maybe Double
+distance (ShortestPathMatrix m) src trg
+    | M.isEmpty m = Nothing
+    | otherwise = toDistance . fst =<< M.index m (src :. trg)
+
+-- | Compute the diameter of a graph from the shortest path matrix of the graph.
+--
+diameter :: ShortestPathMatrix -> Maybe Double
+diameter (ShortestPathMatrix m)
+    | M.isEmpty m = Just 0
+    | otherwise = toDistance $ maximum' $ M.map fst m
+
+-- -------------------------------------------------------------------------- --
+-- Internal
+
+toDistance :: RealFrac a => a -> Maybe a
+toDistance x
+    | x == 1/0 = Nothing
+    | otherwise = Just x
+
+-- | Distance matrix for int inputs.
+--
+intDistMatrix
+    :: Real a
+    => Source r Ix2 a
+    => Array r Ix2 a
+    -> Array M.D Ix2 (Double, Int)
+intDistMatrix = M.imap go
+  where
+    go (x :. y) e
+        | x == y = (0, y)
+        | e > 0 = (realToFrac e, y)
+        | otherwise = (1/0, -1)
+
+-- | Floyd-Warshall With Path Matrix
+--
+-- TODO: use a mutable array?
+-- TODO: implement Dijkstra's algorithm for adj matrix representation.
+--
+floydWarshallInternal
+    :: Array U Ix2 (Double, Int)
+    -> Array U Ix2 (Double,Int)
+floydWarshallInternal a = foldl' go a [0..n-1]
+  where
+    Sz (n :. _) = size a
+
+    go :: Array U Ix2 (Double, Int) -> Int -> Array U Ix2 (Double,Int)
+    go c k = makeArray Seq (Sz (n :. n)) $ \(x :. y) ->
+        let
+            !xy = fst $! c M.! (x :. y)
+            !xk = fst $! c M.! (x :. k)
+            !ky = fst $! c M.! (k :. y)
+            !nxy = snd $! c M.! (x :. y)
+            !nxk = snd $! c M.! (x :. k)
+        in if xy > xk + ky then (xk + ky, nxk) else (xy, nxy)
+
+-- -------------------------------------------------------------------------- --
+-- Floyd Warshall Without Paths (more efficient, by factor of 2)
+
+-- | Floyd Warshall Without Paths (more efficient, by factor of 2).
+--
+distMatrix_
+    :: Source r Ix2 Int
+    => Array r Ix2 Int
+    -> Array M.D Ix2 Double
+distMatrix_ = M.imap go
+  where
+    go (x :. y) e
+        | x == y = 0
+        | e > 0 = realToFrac e
+        | otherwise = 1/0
+
+-- | Floyd Warshall Without Paths (more efficient, by factor of 2).
+--
+-- TODO: use a mutable array?
+-- TODO: implement Dijkstra's algorithm for adj matrix representation.
+--
+floydWarshall_
+    :: Array U Ix2 Double
+    -> Array U Ix2 Double
+floydWarshall_ a = foldl' go a [0..n-1]
+  where
+    Sz (n :. _) = size a
+
+    go :: Array U Ix2 Double -> Int -> Array U Ix2 Double
+    go c k = makeArray Seq (Sz (n :. n)) $ \(x :. y) ->
+        let
+            !xy = c M.! (x :. y)
+            !xk = c M.! (x :. k)
+            !ky = c M.! (k :. y)
+        in if xy > xk + ky then xk + ky else xy
+
+-- | Shortest path matrix.
+--
+-- All entries of the result matrix are either whole numbers or @Infinity@.
+--
+shortestPaths_ :: Array U Ix2 Int -> Array U Ix2 Double
+shortestPaths_ = floydWarshall_ . computeAs U . distMatrix_
+
+-- | Diameter of a graph.
+--
+diameter_ :: Array U Ix2 Int -> Maybe Natural
+diameter_ g
+    | M.isEmpty g = Just 0
+    | otherwise = let x = round $ maximum' $ shortestPaths_ g
+        in if x == round (1/0 :: Double) then Nothing else Just x
diff --git a/src/Data/DiGraph/Random.hs b/src/Data/DiGraph/Random.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/DiGraph/Random.hs
@@ -0,0 +1,133 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- |
+-- Module: Data.DiGraph.Random
+-- Copyright: Copyright © 2019 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+-- Throughout the module an undirected graph is a directed graph that is
+-- symmetric and irreflexive.
+--
+--
+module Data.DiGraph.Random
+(
+-- * Random Regular Graph
+  UniformRng
+, rrgIO
+, rrg
+
+-- * Random Graphs in the \(G_{n,p}\) model
+, gnp
+) where
+
+import Control.Monad
+import Control.Monad.Trans.Class
+import Control.Monad.Trans.Maybe
+
+import qualified Data.Set as S
+
+import Numeric.Natural
+
+import qualified Streaming.Prelude as S
+
+import qualified System.Random.MWC as MWC
+
+-- internal modules
+
+import Data.DiGraph
+
+-- -------------------------------------------------------------------------- --
+-- Utils
+
+-- | Type of a random number generator that uniformily chooses an element from a
+-- range.
+--
+type UniformRng m = (Int, Int) -> m Int
+
+int :: Integral a => Num b => a -> b
+int = fromIntegral
+{-# INLINE int #-}
+
+-- -------------------------------------------------------------------------- --
+-- Random Regular Graph
+
+-- | Undirected, irreflexive random regular graph.
+--
+-- The algorithm here is incomplete. For a complete approach see for instance
+-- https://users.cecs.anu.edu.au/~bdm/papers/RandRegGen.pdf
+--
+rrgIO
+    :: Natural
+    -> Natural
+    -> IO (Maybe (DiGraph Int))
+rrgIO n d = MWC.withSystemRandom $ \gen -> rrg @IO (`MWC.uniformR` gen) n d
+
+-- | Undirected, irreflexive random regular graph.
+--
+-- The algorithm here is incomplete. For a complete approach see for instance
+-- https://users.cecs.anu.edu.au/~bdm/papers/RandRegGen.pdf
+--
+rrg
+    :: Monad m
+    => UniformRng m
+        -- ^ a uniform random number generator
+    -> Natural
+    -> Natural
+    -> m (Maybe (DiGraph Int))
+rrg gen n d = go 0 (S.fromList c) (emptyGraph n)
+  where
+    v = [0 .. int n - 1]
+    c = [(x, y) | x <- v, y <- [0 :: Int .. int d - 1]]
+
+    go i s g
+        | S.null s = return $ Just g
+        | (fst . fst <$> S.minView s) == (fst . fst <$> S.maxView s) = return Nothing
+        | otherwise = sampleEdge s g >>= \case
+            Nothing -> if i < n then go (i + 1) s g else return Nothing
+            Just (s', g') -> go 0 s' g'
+
+    sampleEdge s graph = runMaybeT $ do
+        (s', v₁) <- lift $ uniformSample gen s
+        (s'', v₂) <- lift $ uniformSample gen s'
+        let e₁ = (fst v₁, fst v₂)
+        let e₂ = (fst v₂, fst v₁)
+        guard $ fst v₁ /= fst v₂ && not (isEdge e₁ graph)
+        return (s'', insertEdge e₁ $ insertEdge e₂ graph)
+
+-- | Uniformily sample an element from the input set. Returns the set with the
+-- sampled element removed and the sampled element.
+--
+uniformSample :: Monad m => UniformRng m -> S.Set a -> m (S.Set a, a)
+uniformSample gen s = do
+    p <- gen (0, S.size s - 1)
+    return (S.deleteAt p s, S.elemAt p s)
+
+-- -------------------------------------------------------------------------- --
+-- Gnp
+
+-- | Undirected irreflexive random graph in the \(G_{n,p}\) model.
+--
+gnp
+    :: forall m
+    . Monad m
+    => UniformRng m
+    -> Natural
+    -> Double
+    -> m (DiGraph Int)
+gnp gen n p = S.fold_ (flip insertEdge) (emptyGraph n) id
+    $ S.concat
+    $ S.filterM (const choice)
+    $ S.each
+        [ [(a,b), (b,a)]
+        | a <- [0..int n - 1]
+        , b <- [0..a-1]
+        ]
+  where
+    choice = do
+        v <- gen (0, maxBound)
+        return $ int v <= p * int (maxBound :: Int)
+
diff --git a/test/Data/DiGraph/Random/Test.hs b/test/Data/DiGraph/Random/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/DiGraph/Random/Test.hs
@@ -0,0 +1,300 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE RoleAnnotations #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- |
+-- Module: Data.DiGraph.Random.Test
+-- Copyright: Copyright © 2019 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+-- TODO
+--
+module Data.DiGraph.Random.Test
+( RegularGraph(..)
+, Gnp(..)
+, RandomGraph(..)
+
+-- * Test Properties
+, properties
+) where
+
+import Data.Foldable
+import qualified Data.Graph.Inductive.Graph as G
+import qualified Data.Graph.Inductive.Internal.RootPath as G
+import qualified Data.Graph.Inductive.PatriciaTree as G
+import qualified Data.Graph.Inductive.Query.SP as G
+import Data.Hashable
+import Data.Massiv.Array (Array(..), U, Ix2(..), makeArray, Comp(..))
+import qualified Data.Massiv.Array as M
+import Data.Proxy
+#if !MIN_VERSION_base(4,11,0)
+import Data.Semigroup
+#endif
+
+import GHC.Generics
+import GHC.TypeNats
+
+import Numeric.Natural
+
+import Test.QuickCheck
+
+import Text.Printf
+
+-- internal modules
+
+import Data.DiGraph
+import qualified Data.DiGraph.FloydWarshall as FW
+import Data.DiGraph.Random
+
+-- -------------------------------------------------------------------------- --
+-- Utils
+
+int :: Integral a => Num b => a -> b
+int = fromIntegral
+{-# INLINE int #-}
+
+-- -------------------------------------------------------------------------- --
+-- Arbitrary Graph Instances
+
+-- | Uniformily distributed regular graph in edge list representation. The type
+-- parameter is the degree of the graph. The size parameter of the QuickCheck
+-- Arbirary instance is the order of the graph.
+--
+type role RegularGraph phantom
+newtype RegularGraph (d :: Nat) = RegularGraph { getRegularGraph :: DiGraph Int }
+    deriving (Show, Eq, Ord, Generic)
+
+instance KnownNat d => Arbitrary (RegularGraph d) where
+    arbitrary = fmap RegularGraph
+        $ scale (if d > (0 :: Int) then (+ (d + 1)) else id)
+        $ sized $ \n -> maybe discard return =<< rrg choose (int n) d
+      where
+        d :: Num a => a
+        d = int $ natVal (Proxy @d)
+
+
+-- | Random graph in the \(G_{n,p}\) model in edge list representation. The type
+-- parameter is the size of the graph. The size parameter of the QuickCheck
+-- Arbitrary instance is the expected size of the graph (number of edges).
+--
+-- Notes:
+--
+-- For \(n * (n-1) * p / 2) = m\) the distributions \(G_{n,p}\) and \(G_{n,m}\)
+-- have very similar properties.
+--
+type role Gnp phantom
+newtype Gnp (n :: Nat) = Gnp { getGnp :: DiGraph Int }
+    deriving (Show, Eq, Ord, Generic)
+
+instance KnownNat n => Arbitrary (Gnp n) where
+    arbitrary = sized $ \m -> Gnp <$> gnp choose n (p m)
+      where
+        -- m = p * n * (n-1) / 2
+        p :: Int -> Double
+        p m = min 1 ((2 * int m) / (n * (n - 1)))
+
+        n :: Num a => a
+        n = int $ natVal $ Proxy @n
+
+-- | The uniform distribution over all undirected graphs in edge list
+-- representation. This is the \(G_{n,1/2})\) random graph.
+--
+-- Note for graphs in the \(G_{n,1/2}\) model many important properties are
+-- highly concentrated around their expectation. For instances, almost all
+-- graphs of a large enough order \(n\) are connected. Therefore, in many cases
+-- this is not a good testing model for test spaces where only a low coverage is
+-- possible.
+--
+newtype RandomGraph = RandomGraph { getRandomGraph :: DiGraph Int }
+    deriving (Show, Eq, Ord, Generic)
+
+instance Arbitrary RandomGraph where
+    arbitrary = sized $ \n -> RandomGraph <$> gnp choose (int n) 0.5
+
+{-
+-- | Random graph in the \(G_{n,m}\) model. The type parameter is the order
+-- of the graph. The \(m\) parameter free at runtime and can be chosen
+-- set to the @size@ parameter in 'Arbitrary' instances.
+--
+-- The random graph \(G_{n,m}\) is the uniform distribution over graphs of
+-- order \(n\) and size \(m\).
+--
+type role Gnm phantom
+newtype Gnm (n :: Nat) = Gnm { getGnm :: DiGraph Int }
+
+instance (KnownNat n) => Arbitrary (Gnm n) where
+    arbitrary = sized $ \m -> d
+-}
+
+-- -------------------------------------------------------------------------- --
+-- Dijkstra's Shortest Path Algorithm
+--
+
+-- | On sparse Graphs Dijkstra's algorithm is, generally, much faster than
+-- Floyd-Warshall. The FGL implementation, however, is very inefficient. So,
+-- this is almost certainly slower than the implementation of Floyd-Warshall
+-- above.
+--
+-- We include this only for testing the correctness short test path matrix
+-- implementation in "Data.DiGraph" which is based on Floyd-Warshall.
+--
+-- All entries of the result matrix are either whole numbers or @Infinity@.
+--
+fglShortestPaths :: G.Graph g => g Int Int -> Array U Ix2 Double
+fglShortestPaths g = makeArray Seq (M.Sz (n :. n)) $ \(i :. j) ->
+    maybe (1/0) realToFrac $ G.getDistance j (sp i)
+  where
+    sp i = G.spTree i g
+    n = G.order g
+
+fglDiameter :: G.Graph g => g Int Int -> Maybe Natural
+fglDiameter g = if M.isEmpty sps
+    then Just 0
+    else let x = round $ M.maximum' sps
+        in if x == round (1/0 :: Double) then Nothing else Just x
+  where
+    sps = fglShortestPaths g
+
+toFglGraph :: G.Graph g => DiGraph Int -> g Int Int
+toFglGraph g = G.mkGraph vs es
+  where
+    vs = [(i,i) | i <- toList (vertices g)]
+    es = concatMap (\(x,y) -> [(x, y, 1)]) $ edges g
+
+-- -------------------------------------------------------------------------- --
+-- Tools for collecting coverage data
+
+collect_graphStats
+    :: Testable prop
+    => Natural
+        -- ^ maximum value for the graph order of in the graph distribution
+    -> DiGraph Int
+    -> prop
+    -> Property
+collect_graphStats n g
+    = collect (orderClass (n + 1) g)
+    . collect (sizeClass (n + 1) g)
+    . collect (densityClass g)
+    . collect (diameterClass g)
+
+-- | For undirected graphs. In steps of tenth.
+--
+densityClass :: Eq a => Hashable a => DiGraph a -> String
+densityClass g
+    | order g == 0 = "density: null graph"
+    | otherwise = printf "density: %.1f" d
+  where
+    o :: Num a => a
+    o = int (order g)
+
+    d :: Double
+    d = 2 * int (size g) / (o * (o - 1))
+
+diameterClass :: Eq a => Hashable a => DiGraph a -> String
+diameterClass g = "diameter: " <> show (diameter g)
+
+orderClass :: Natural -> DiGraph a -> String
+orderClass n g = classifyByRange "order" n (order g)
+
+-- | undirected size
+--
+sizeClass :: Eq a => Hashable a => Natural -> DiGraph a -> String
+sizeClass n g = classifyByRange "size" (n * (n - 1) `div` 2) (size g)
+
+classifyByRange :: String -> Natural -> Natural -> String
+classifyByRange s n x = printf "%s: (%d, %d)" s l u
+  where
+    l = (x * 10 `div` n) * (n `div` 10)
+    u = ((x * 10 `div` n) + 1) * (n `div` 10)
+
+-- -------------------------------------------------------------------------- --
+-- Property Utils
+
+graphProperty
+    :: Testable prop
+    => Natural
+        -- ^ maximum value for the graph order in the graph distribution
+    -> (DiGraph Int -> prop)
+    -> Property
+graphProperty maxN p = mapSize (`mod` int maxN) $ \(RandomGraph g) ->
+    collect_graphStats maxN g $ p g
+
+rrgProperty
+    :: Testable prop
+    => Natural
+        -- ^ maximum value for the graph order of in the graph distribution
+    -> (DiGraph Int -> prop)
+    -> Property
+rrgProperty maxN p = mapSize (`mod` int maxN) $ \(RegularGraph g :: RegularGraph 3) ->
+    collect_graphStats maxN g $ p g
+
+-- | The first type parameter is the static order of the graph. The size of the
+-- QuichCheck Arbitrary instances is the expected size of the graph (number of
+-- edges).
+--
+gnpProperty
+    :: forall (n :: Nat) prop
+    . KnownNat n
+    => Testable prop
+    => (DiGraph Int -> prop)
+    -> Property
+gnpProperty p = property $ \(Gnp g :: Gnp n) ->
+    collect_graphStats (natVal $ Proxy @n) g $ p g
+
+-- -------------------------------------------------------------------------- --
+-- Properties of graph algorithms
+
+prop_shortestPaths :: DiGraph Int -> Property
+prop_shortestPaths g = fglShortestPaths fglG === M.computeAs M.U (M.map fst m)
+  where
+    fglG = toFglGraph @G.Gr g
+    denseG = FW.fromAdjacencySets $ adjacencySets g
+    FW.ShortestPathMatrix m = FW.floydWarshall denseG
+
+prop_diameter :: DiGraph Int -> Property
+prop_diameter g = fglDiameter (toFglGraph @G.Gr g) === diameter g
+
+properties_randomGraph :: [(String, Property)]
+properties_randomGraph = prefix "uniform random graph" <$>
+    [ ("isDiGraph", graphProperty 20 isDiGraph)
+    , ("isSymmetric", graphProperty 20 isSymmetric)
+    , ("shortestPaths", graphProperty 20 prop_shortestPaths)
+    , ("diameter", graphProperty 20 prop_diameter)
+    ]
+
+properties_gnp :: [(String, Property)]
+properties_gnp = prefix "Gnp random graph" <$>
+    [ ("isDiGraph", gnpProperty @20 isDiGraph)
+    , ("isSymmetric", gnpProperty @20 isSymmetric)
+    , ("shortestPaths", gnpProperty @20 prop_shortestPaths)
+    , ("diameter", gnpProperty @20 prop_diameter)
+    ]
+
+properties_rrg :: [(String, Property)]
+properties_rrg = prefix "random regular graph" <$>
+    [ ("isDiGraph", rrgProperty 20 isDiGraph)
+    , ("isRegular", rrgProperty 20 isRegular)
+    , ("isSymmetric", rrgProperty 20 isSymmetric)
+    , ("shortestPaths", rrgProperty 20 prop_shortestPaths)
+    , ("diameter", rrgProperty 20 prop_diameter)
+    ]
+
+prefix :: String -> (String, b) -> (String, b)
+prefix a (b, c) = (a <> "." <> b, c)
+
+-- | Test properties.
+--
+properties :: [(String, Property)]
+properties = mconcat
+    [ properties_randomGraph
+    , properties_gnp
+    , properties_rrg
+    ]
+
diff --git a/test/Data/DiGraph/Test.hs b/test/Data/DiGraph/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/DiGraph/Test.hs
@@ -0,0 +1,109 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Module: Data.DiGraph.Test
+-- Copyright: Copyright © 2019 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+module Data.DiGraph.Test
+(
+-- * Test Properties
+  properties
+) where
+
+import Data.Bifunctor
+import Data.Hashable
+#if !MIN_VERSION_base(4,11,0)
+import Data.Semigroup
+#endif
+
+import Numeric.Natural
+
+import Test.QuickCheck
+
+-- internal modules
+
+import Data.DiGraph
+
+-- -------------------------------------------------------------------------- --
+-- Test Properties
+
+prefixProperties :: String -> [(String, Property)] -> [(String, Property)]
+prefixProperties p = fmap $ first (p <>)
+
+properties_undirected :: Eq a => Hashable a => DiGraph a -> [(String, Property)]
+properties_undirected g =
+    [ ("isDiGraph", property $ isDiGraph g)
+    , ("isIrreflexive", property $ isIrreflexive g)
+    , ("isSymmetric", property $ isSymmetric g)
+    ]
+
+properties_emptyGraph :: Natural -> [(String, Property)]
+properties_emptyGraph n = prefixProperties ("emptyGraph of order " <> show n <> ": ")
+    $ ("order == " <> show n, order g === n)
+    : ("size == 0", size g === 0)
+    : properties_undirected g
+  where
+    g = emptyGraph n
+
+properties_singletonGraph :: [(String, Property)]
+properties_singletonGraph = prefixProperties "singletonGraph: "
+    $ ("order == 1", order g === 1)
+    : ("size == 0", symSize g === 0)
+    : ("outDegree == 0", maxOutDegree g === 0)
+    : ("isRegular", property $ isRegular g)
+    : ("diameter == 0", diameter g === Just 0)
+    : properties_undirected g
+  where
+    g = singleton
+
+properties_petersonGraph :: [(String, Property)]
+properties_petersonGraph = prefixProperties "petersonGraph: "
+    $ ("order == 10", order g === 10)
+    : ("size == 15", symSize g === 15)
+    : ("outDegree == 3", maxOutDegree g === 3)
+    : ("isRegular", property $ isRegular g)
+    : ("diameter == 2", diameter g === Just 2)
+    : properties_undirected g
+  where
+    g = petersonGraph
+
+properties_twentyChainGraph :: [(String, Property)]
+properties_twentyChainGraph = prefixProperties "twentyChainGraph: "
+    $ ("order == 20", order g === 20)
+    : ("size == 30", symSize g === 30)
+    : ("outDegree == 3", maxOutDegree g === 3)
+    : ("isRegular", property $ isRegular g)
+    : ("diameter == 2", diameter g === Just 4)
+    : properties_undirected g
+  where
+    g = twentyChainGraph
+
+properties_hoffmanSingletonGraph :: [(String, Property)]
+properties_hoffmanSingletonGraph = prefixProperties "HoffmanSingletonGraph: "
+    $ ("order == 50", order g === 50)
+    : ("size == 175", symSize g === 175)
+    : ("outDegree == 7", maxOutDegree g === 7)
+    : ("isRegular", property $ isRegular g)
+    : ("diameter == 2", diameter g === Just 2)
+    : properties_undirected g
+  where
+    g = hoffmanSingleton
+
+-- | Test Properties.
+--
+properties :: [(String, Property)]
+properties = (concat :: [[(String, Property)]] -> [(String, Property)])
+    [ properties_emptyGraph 0
+    , properties_emptyGraph 2
+    , properties_singletonGraph
+    , properties_petersonGraph
+    , properties_twentyChainGraph
+    , properties_hoffmanSingletonGraph
+    ]
+
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Module: Main
+-- Copyright: Copyright © 2019 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+module Main
+( main
+) where
+
+import Data.Bitraversable
+#if !MIN_VERSION_base(4,11,0)
+import Data.Semigroup
+#endif
+
+import System.Exit
+
+import Test.QuickCheck
+
+-- internal modules
+
+import qualified Data.DiGraph.Test (properties)
+import qualified Data.DiGraph.Random.Test (properties)
+
+-- -------------------------------------------------------------------------- --
+-- Support for QuickCheck < 2.12
+
+#if ! MIN_VERSION_QuickCheck(2,12,0)
+isSuccess :: Result -> Bool
+isSuccess Success{} = True
+isSuccess _ = False
+#endif
+
+-- -------------------------------------------------------------------------- --
+-- Main
+
+main :: IO ()
+main = do
+    results <- traverse (bitraverse print quickCheckResult) properties
+    if and $ isSuccess . snd <$> results
+        then exitSuccess
+        else exitFailure
+
+properties :: [(String, Property)]
+properties = mconcat
+    [ prefix "Data.DiGraph.Test" <$> Data.DiGraph.Test.properties
+    , prefix "Data.DiGraph.Random.Test" <$> Data.DiGraph.Random.Test.properties
+    ]
+  where
+    prefix a (b, c) = (a <> "/" <> b, c)
