Ticket #4131: QuickHull.hs

File QuickHull.hs, 7.3 KB (added by benl, 3 years ago)
Line 
1{-# LANGUAGE BangPatterns, PatternGuards, RankNTypes #-}
2{-# OPTIONS  -fwarn-unused-imports #-}
3module QuickHull
4        (quickHull)
5where
6import Control.Monad
7import Control.Exception
8import Control.Concurrent
9import Control.Monad.ST
10import GHC.Conc
11import Data.IORef
12import Data.List
13import Data.Ord
14import Data.Vector.Unboxed                      (Vector)
15import qualified Data.Vector.Unboxed            as V
16import qualified Data.Vector.Unboxed.Mutable    as MV
17import qualified Data.Vector.Generic            as G
18
19type Point      = (Double, Double)
20type Line       = (Point, Point)
21
22
23-- | Compute the convex hull of a vector of points.
24quickHull :: Vector Point -> IO (Vector Point)
25quickHull !points
26  | V.length points == 0       
27  = return points
28
29  | otherwise
30  = do  -- Find the left and right-most points.
31        let (minx, maxx)        = minmax points
32
33        -- Hull points get written to the vector in this IORef.
34        hullRef <- newIORef V.empty
35
36        -- Fork off computations to handle half of the points each.
37        -- For uniformly distributed points this first iteration takes most of the time.
38        parIO   [ hsplit hullRef points minx maxx
39                , hsplit hullRef points maxx minx]
40
41        -- Grab the finished hull points.
42        hull    <- readIORef hullRef
43
44        -- We've got the hull points, but they can appear in arbitrary order.
45        -- Do a rubbish via-lists merge phase so that they appear clockwise around the edge.
46        -- This isn't too expensive if there aren't many points on the hull.
47        let (above, below) 
48                = V.unstablePartition
49                        (\p -> distance minx maxx p > 0)
50                        hull
51       
52        let aboveSorted = V.fromList $ sortBy (comparing fst) $ V.toList above
53        let belowSorted = V.fromList $ sortBy (comparing fst) $ V.toList below
54        let hull' = aboveSorted V.++ V.reverse belowSorted
55
56        return hull'
57       
58
59hsplit :: IORef (Vector Point) -> Vector Point -> Point -> Point -> IO ()
60{-# INLINE hsplit #-}
61hsplit hullRef !points !p1@(!p1X, !p1Y) !p2@(!p2X, !p2Y)
62        -- we've found one.
63        | V.length packed == 0
64        = addHullPoint hullRef p1
65       
66        -- do the two new segments in parallel.
67        | V.length packed > 1000
68        = parIO
69                [ hsplit hullRef packed p1 pm
70                , hsplit hullRef packed pm p2 ]
71               
72        | otherwise
73        = do    hsplit hullRef packed p1 pm
74                hsplit hullRef packed pm p2
75
76        where   (packed, pm)    = parPackPoints points p1X p1Y p2X p2Y
77       
78
79-- | Copy points from the input vector that are on the left of the line into a
80--      new buffer. While we're doing this, determine the point that is furthest
81--      from the line.
82--
83--      If we have a big enough vector then split it in two and do both halves
84--      in parallel. Doing this requires a copy afterwards to join the two
85--      results back together.
86--
87parPackPoints 
88        :: Vector Point 
89        -> Double -> Double
90        -> Double -> Double
91        -> ( Vector Point
92           , Point)
93       
94{-# INLINE parPackPoints #-}
95parPackPoints !points !p1X !p1Y !p2X !p2Y
96 |   numCapabilities == 1
97  || V.length points < 1000
98 = packPoints p1X p1Y p2X p2Y points
99
100 | otherwise
101 = let 
102        numSegments     = numCapabilities
103
104        -- Total number of points to process.
105        lenPoints       = V.length points
106
107        -- How many points to process in each segment.
108        lenSeg          = lenPoints `div` numSegments
109
110        -- If the total number of points doesn't divide evenly into segments
111        -- then there may be an odd number. Make sure to get the rest into the last segment.
112        splitPacked count ixStart
113            | count == 0        = []
114
115            | count == 1       
116            = let points'               = V.slice ixStart (lenPoints - ixStart) points
117                  result@(packed', _)   = packPoints p1X p1Y p2X p2Y points'
118              in  packed' `pseq` (result : [])
119
120            | otherwise
121            = let points'               = V.slice ixStart lenSeg points
122                  result@(packed', _)   = packPoints p1X p1Y p2X p2Y points'
123                  rest                  = splitPacked (count - 1) (ixStart + lenSeg)
124              in  packed' `par` rest `par` (result : rest)
125
126        results = splitPacked numSegments 0
127        vResult = concatVectors $ map fst results
128        pMax    = selectFurthest p1X p1Y p2X p2Y results
129       
130   in   (vResult, pMax)
131
132
133selectFurthest 
134        :: Double -> Double 
135        -> Double -> Double
136        -> [(Vector Point, Point)] 
137        -> Point
138       
139selectFurthest !p1X !p1Y !p2X !p2Y ps
140 = go (0, 0) 0 ps
141
142 where  go pMax !distMax []     
143         = pMax
144
145        go pMax !distMax ((packed, pm):rest)
146         | V.length packed == 0
147         = go pMax distMax rest
148       
149         | otherwise
150         , dist         <-  distance (p1X, p1Y) (p2X, p2Y) pm
151         = if dist > distMax
152                then go pm   dist    rest
153                else go pMax distMax rest
154
155
156packPoints 
157        :: Double -> Double             -- First point on dividing line.
158        -> Double -> Double             -- Second point on dividing line.
159        -> Vector Point                 -- Source points.
160        -> ( Vector Point               -- Packed vector containing only points on the left of the line.
161           , Point)                     -- The point on the left that was furthest from the line.
162
163{-# INLINE packPoints #-}
164packPoints !p1X !p1Y !p2X !p2Y !points
165 = let
166        result 
167         = G.create
168         $ do   packed           <- MV.new (V.length points + 1)
169                (pMax, ixPacked) <- fill points packed p1X p1Y p2X p2Y 0 0
170
171                -- We stash the maximum point on the end of the vector to get
172                -- it through the create call.
173                MV.write packed ixPacked pMax
174                return $ MV.slice 0 (ixPacked + 1) packed
175       
176   in   ( V.slice 0 (V.length result - 1) result
177        , result V.! (V.length result - 1))
178                       
179
180fill    :: forall s
181        .  Vector Point                 -- Source points.
182        -> MV.MVector s Point           -- Vector to write packed points into.
183        -> Double -> Double             -- First point on dividing line.
184        -> Double -> Double             -- Second poitn on dividing line.
185        -> Int                          -- Index into source points to start reading from.
186        -> Int                          -- Index into packed points to start writing to.
187        -> ST s
188                ( Point                 -- Furthest point from the line that was found.
189                , Int)                  -- The number of packed points written.
190
191{-# INLINE fill #-}
192fill !points !packed !p1X !p1Y !p2X !p2Y !ixPoints' !ixPacked'
193 = go (0, 0) 0 ixPoints' ixPacked'
194 where go pMax !distMax !ixPoints !ixPacked
195        | ixPoints >= V.length points   
196        = do    return (pMax, ixPacked)
197               
198        | p     <- points V.! ixPoints
199        , d     <- distance (p1X, p1Y) (p2X, p2Y) p
200        , d > 0
201        = do    MV.write packed ixPacked p
202                if d > distMax
203                 then   go p    d       (ixPoints + 1) (ixPacked + 1)
204                 else   go pMax distMax (ixPoints + 1) (ixPacked + 1)
205                       
206        | otherwise
207        = go pMax distMax (ixPoints + 1) ixPacked
208
209
210minmax :: Vector Point -> (Point, Point)
211{-# INLINE minmax #-}
212minmax !vec
213 = go first first 0
214 where  first   = vec V.! 0
215
216        go pMin@(!minX, !minY) pMax@(!maxX, !maxY) !ix
217          | ix >= V.length vec  = (pMin, pMax)
218
219          | (x, y)      <- vec V.! ix
220          = if       x < minX then go (x, y) pMax   (ix + 1)
221            else if  x > maxX then go pMin   (x, y) (ix + 1)
222            else go pMin pMax (ix + 1)
223       
224
225distance :: Point -> Point -> Point -> Double
226{-# INLINE distance #-}
227distance (x1, y1) (x2, y2) (xo, yo)
228  = (x1-xo) * (y2 - yo) - (y1 - yo) * (x2 - xo)
229
230
231addHullPoint :: IORef (Vector Point) -> Point -> IO ()
232addHullPoint hullRef p
233 = atomicModifyIORef hullRef
234 $ \hull -> (V.singleton p V.++ hull, ())
235
236
237
238-- Can't find an equivalent for this in Control.Concurrent.
239parIO :: [IO ()] -> IO ()
240parIO stuff
241 = do   mVars   <- replicateM (length stuff) newEmptyMVar
242        zipWithM_ (\c v -> forkIO $ c `finally` putMVar v ()) stuff mVars
243        mapM_ readMVar mVars
244       
245
246-- We really want a function in the vector library for this.
247concatVectors :: [Vector Point] -> Vector Point
248{-# NOINLINE concatVectors #-}
249concatVectors vectors
250 = G.create
251 $ do   let len = sum $ map V.length vectors
252        vOut    <- MV.new len
253        go vectors vOut 0
254        return vOut
255
256 where  {-# INLINE go #-}
257        go [] _ _
258         = return ()
259
260        go (vSrc:vsSrc) vDest !ixStart 
261         = do   let lenSrc      = V.length vSrc
262                let vDestSlice  = MV.slice ixStart lenSrc vDest
263                V.copy vDestSlice vSrc
264                go vsSrc vDest (ixStart + lenSrc)