packages feed

moonlight-triangulation-1.4.0.2: bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Support.hs

{-# LANGUAGE DataKinds #-}

-- | Input generation, canonical encoding, and the shared build helpers.
module Moonlight.Triangulation.Bench.SpadeCompare.Support where

import Data.List (sort)
import Data.Word (Word64)
import GHC.Float (castDoubleToWord64)
import Numeric (showHex)
import qualified Data.Vector as V
import Moonlight.Triangulation
import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
import Moonlight.Triangulation.HintGenerator

-- | The same generated points flattened onto a sliver a picometre thick. No
-- coordinate becomes equal to another and no four points become cocircular, so
-- the input stays in general position and the load stays on the ordinary
-- face-building path; what collapses is the angular spread the bulk loaders
-- index their hulls by. That collapse is the whole of what this input
-- measures, and it does not reach the face-less location path — that one needs
-- exact collinearity, which 'exactlyCollinearPoints' supplies and gates.
nearCollinearPoints :: Word64 -> Int -> [Point]
nearCollinearPoints seed count =
  [Point x (y * 1.0e-12) | Point x y <- randomPoints seed count]

-- | Points on one exact line: @(i, 0)@, every coordinate an integer that a
-- 'Double' holds exactly. No three are in general position, so 'orient2d'
-- returns an exact zero on both sides and no face is ever built.
--
-- Uniqueness does not need general position, only one valid answer. A
-- collinear set admits exactly one triangulation — the chain of consecutive
-- segments, @n - 1@ of them and no triangle — so the canonical edge set is
-- fully determined and gates bit for bit against the strict side. Both
-- implementations state that same count as their own degenerate Euler
-- invariant, which is what makes this a shared answer rather than a
-- coincidence.
exactlyCollinearPoints :: Int -> [Point]
exactlyCollinearPoints count = [Point (fromIntegral index) 0 | index <- [0 .. count - 1]]

hex64 :: Double -> String
hex64 value =
  let raw = showHex (castDoubleToWord64 value) ""
   in replicate (16 - length raw) '0' <> raw

-- | Canonical edge set: each undirected edge as its two endpoint coordinates in
-- IEEE-754 bit patterns, endpoint-ordered then globally sorted. Formatting
-- cannot introduce a spurious disagreement and rounding cannot hide a real one.
canonicalEdges :: Triangulation mode vertex directed undirected face -> [String]
canonicalEdges triangulation =
  sort
    [ encodeCanonicalEdge first second
    | edge <- undirectedEdges triangulation
    , let (from, to) = undirectedEndpoints triangulation edge
          p = vertexPoint triangulation from
          q = vertexPoint triangulation to
          (first, second) = if key p <= key q then (p, q) else (q, p)
    ]
 where
  key (Point x y) = (x, y)

canonicalConstraintEdges
  :: ConstrainedDelaunayTriangulation vertex
  -> [String]
canonicalConstraintEdges triangulation =
  sort
    [ encodeCanonicalEdge first second
    | edge <- constraintEdges triangulation
    , let (from, to) = undirectedEndpoints triangulation edge
          p = vertexPoint triangulation from
          q = vertexPoint triangulation to
          (first, second) =
            if pointKey p <= pointKey q
              then (p, q)
              else (q, p)
    ]
 where
  pointKey (Point x y) = (x, y)

encodeCanonicalEdge :: Point -> Point -> String
encodeCanonicalEdge (Point ax ay) (Point bx by) =
  concatMap hex64 [ax, ay, bx, by]

coordHex :: Point -> String
coordHex (Point x y) = hex64 x <> hex64 y

-- | A cycle, rotated to begin at its smallest encoded element.
--
-- Both implementations keep one entry edge per vertex and per face — whichever
-- their builder wrote last — so the two walk the same cycle from different
-- places. Rotating pins the order, which the mesh determines, and declines to
-- pin the starting point, which it does not. The rotation compares the encoded
-- strings rather than the coordinates so that both sides order by the same
-- comparison: the bit pattern of a negative double does not sort in numeric
-- order, so a Haskell 'Ord' on 'Point' and a Rust @partial_cmp@ on a tuple
-- would each have to be argued to agree where two byte strings simply do.
rotateToSmallest :: [String] -> [String]
rotateToSmallest [] = []
rotateToSmallest ring =
  let !smallest = minimum ring
      (before, after) = break (== smallest) ring
   in after <> before

require :: Show failure => Either failure value -> IO value
require (Left failure) = fail (show failure)
require (Right value) = pure value

requireQueryPoints
  :: Traversable container
  => container Point
  -> IO (container QueryPoint)
requireQueryPoints = require . traverse mkQueryPoint

requireQueryChords
  :: Traversable container
  => container (Point, Point)
  -> IO (container (QueryPoint, QueryPoint))
requireQueryChords =
  require
    . traverse
      (\(from, to) -> (,) <$> mkQueryPoint from <*> mkQueryPoint to)

delaunayOf :: Int -> IO (DelaunayTriangulation Point)
delaunayOf count =
  buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x9e3779b97f4a7c15 count)))

-- | One line per query: the coordinates of the nearest vertex the hierarchy
-- routed the search to, or @none@.
nearestAnswer
  :: DelaunayTriangulation Point
  -> HierarchyHint
  -> QueryPoint
  -> String
nearestAnswer triangulation hierarchy query =
  let hint = case hierarchyHint hierarchy query of
        Just (VertexHint vertex) -> Just vertex
        _ -> Nothing
   in case nearestNeighbor triangulation hint query of
        Nothing -> "none"
        Just (vertex, _) ->
          let Point x y = vertexPoint triangulation vertex
           in hex64 x <> hex64 y

pointHex :: Point -> String
pointHex (Point x y) = hex64 x <> "," <> hex64 y

constraintPairs :: Int -> Int -> [(Int, Int)]
constraintPairs pointCount constraintCount =
  take constraintCount
    [ (a, b)
    | index <- [0 ..]
    , let a = index `mod` pointCount
          b = (index * 6151 + pointCount `quot` 2) `mod` pointCount
    , a /= b
    ]

-- | A closed square with no dangling constraint. The interior segment used by
-- the package's own benchmark is outside spade's documented "closed shape"
-- contract and makes it refuse the whole domain — not a workload the two can be
-- timed on.
refinementInput :: (V.Vector Point, V.Vector (Int, Int))
refinementInput =
  ( V.fromList [Point 0 0, Point 64 0, Point 64 64, Point 0 64]
  , V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]
  )