packages feed

moonlight-triangulation-1.0.1.0: test/algebra/Moonlight/Triangulation/AlgebraFixtures.hs

-- | The operand meshes and mesh observations both algebra slices are stated
-- over, built against the facade alone.
module Moonlight.Triangulation.AlgebraFixtures
  ( Mesh
  , PointMesh
  , meshOf
  , pointMeshOf
  , operands
  , separatedOperands
  , cocircularRing
  , collinearSites
  , latticeSites
  , pointsOf
  , dedupeAscending
  , siteList
  , siteSet
  , edgeKeys
  , assertMesh
  , assertMeshEquivalent
  , pairs
  , triples
  , advance
  , scramble
  , randomSites
  ) where

import Data.List (sortBy)
import Data.Ord (comparing)
import qualified Data.Set as Set
import qualified Data.Vector as V
import Data.Word (Word64)
import Moonlight.Triangulation
  ( DelaunayTriangulation
  , Point (Point)
  , VertexId
  , buildTriangulation
  , canonicalize
  , delaunay
  , delaunayGeometry
  , numFaces
  , numUndirectedEdges
  , numVertices
  , undirectedEdges
  , undirectedEndpoints
  , unitElementDefaults
  , vertexPoint
  , vertices
  )
import Support (requireRight)

-- | The carrier. Geometry and nothing else: no vertex payload to need a
-- commutative combining rule, no element payloads to survive a rewrite that
-- destroys the elements they labelled.
type Mesh = DelaunayTriangulation ()

-- | The same carrier with its exact coordinate retained as the vertex
-- annotation. Annotation-preserving set laws use it to construct their
-- expected values without reaching below the public facade.
type PointMesh = DelaunayTriangulation (Point)

-- ── operands ─────────────────────────────────────────────────────────────────

-- | The site families the laws are exercised over.
--
-- Sized so that the cubic sweep in the associativity law stays cheap. Coverage
-- here is by /kind/ of degeneracy, not by count: what breaks a join is a
-- cocircular quad whose tie-break went the other way, or a mesh with no faces
-- at all, and neither of those becomes more likely at ten thousand sites.
families :: [(String, [Point])]
families =
  [ ("void", [])
  , ("single", [Point 3 (-7)])
  , ("pair", [Point 0 0, Point 4 1])
  , ("collinear", collinearSites)
  , ("cocircular", cocircularRing)
  , ("lattice", latticeSites)
  , ("repeated", concatMap (replicate 3) [Point 0 0, Point 5 0, Point 0 5, Point 5 5, Point 2 3])
  , ("scattered", randomSites 0xC0FFEEBABE 32)
  , ("extreme", [Point 1.0e-8 1.0e-8, Point 1.0e8 (-1.0e8), Point (-1.0e8) 1.0e8, Point 0 0, Point 1 1])
  ]

-- | Exactly cocircular lattice points on @x² + y² = 625@.
--
-- Trigonometric points would be cocircular only to within rounding, and the
-- rule this is here to exercise — the diagonal tie-break that fires when the
-- lifted quadrilateral is exactly flat — would then never fire at all.
cocircularRing :: [Point]
cocircularRing =
  [ Point (fromIntegral (signX * x)) (fromIntegral (signY * y))
  | (x, y) <- [(25, 0), (0, 25), (7, 24), (24, 7), (15, 20), (20, 15)] :: [(Int, Int)]
  , signX <- [1, -1]
  , signY <- [1, -1]
  ]

collinearSites :: [Point]
collinearSites = [Point (fromIntegral k) (2 * fromIntegral k - 1) | k <- [0 .. 9 :: Int]]

latticeSites :: [Point]
latticeSites = [Point (fromIntegral i) (fromIntegral j) | i <- [0 .. 4 :: Int], j <- [0 .. 4 :: Int]]

-- | The meshes every pairwise and triple law runs over.
--
-- Chosen for overlap structure rather than for size: disjoint operands, nested
-- ones, partially overlapping ones, the empty one, and — the case that matters
-- most — two meshes standing on the /same/ sites built in different orders, so
-- they are geometrically identical and structurally distinct.
operands :: IO [(String, Mesh)]
operands = do
  let sites name = maybe [] id (lookup name families)
      lattice = sites "lattice"
  void' <- meshOf "void" []
  single <- meshOf "single" (sites "single")
  collinear <- meshOf "collinear" (sites "collinear")
  ring <- meshOf "cocircular" (sites "cocircular")
  lower <- meshOf "lattice-lower" (take 15 lattice)
  upper <- meshOf "lattice-upper" (drop 10 lattice)
  repeated <- meshOf "repeated" (sites "repeated")
  scatterA <- meshOf "scattered" (sites "scattered")
  scatterB <- meshOf "scattered-scrambled" (scramble 0x5EED (sites "scattered"))
  pure
    [ ("void", void')
    , ("single", single)
    , ("collinear", collinear)
    , ("cocircular", ring)
    , ("lattice-lower", lower)
    , ("lattice-upper", upper)
    , ("repeated", repeated)
    , ("scattered", scatterA)
    , ("scattered-scrambled", scatterB)
    ]

-- | Operand pairs whose sites are separated by a vertical line, which is the
-- stratum a seam schedule is defined on and the one the old-edge identity is
-- sharpest over: with no shared site, every edge is unambiguously left, right
-- or cross.
separatedOperands :: IO [((String, Mesh), (String, Mesh))]
separatedOperands =
  traverse
    ( \(name, seed, count, shift) -> do
        let sites = randomSites seed count
        left <- meshOf (name <> "-left") sites
        right <- meshOf (name <> "-right") [Point (x + shift) y | Point x y <- sites]
        pure ((name <> "-left", left), (name <> "-right", right))
    )
    [ ("split-distant", 0x51DE1, 60, 1000)
    , ("split-near", 0x51DE2, 60, 3)
    , ("split-abutting", 0x51DE3, 60, 2.05)
    ]

-- ── construction ─────────────────────────────────────────────────────────────

meshOf :: String -> [Point] -> IO Mesh
meshOf label points =
  requireRight ("build geometry " <> label) (delaunayGeometry (V.fromList points))

pointMeshOf :: String -> [Point] -> IO PointMesh
pointMeshOf label points =
  buildTriangulation
    <$> requireRight ("build " <> label) (delaunay unitElementDefaults (V.fromList points))

pointsOf :: [(Double, Double)] -> [Point]
pointsOf keys = [Point x y | (x, y) <- keys]

dedupeAscending :: [(Double, Double)] -> [Point]
dedupeAscending sorted = [Point x y | (x, y) <- dropAdjacentDuplicates sorted]

dropAdjacentDuplicates :: Eq a => [a] -> [a]
dropAdjacentDuplicates (first : second : rest)
  | first == second = dropAdjacentDuplicates (second : rest)
  | otherwise = first : dropAdjacentDuplicates (second : rest)
dropAdjacentDuplicates rest = rest

-- ── observation ──────────────────────────────────────────────────────────────

siteKey :: Mesh -> VertexId -> (Double, Double)
siteKey mesh vertex = let Point x y = vertexPoint mesh vertex in (x, y)

siteList :: Mesh -> [(Double, Double)]
siteList mesh = [siteKey mesh vertex | vertex <- vertices mesh]

siteSet :: Mesh -> Set.Set (Double, Double)
siteSet = Set.fromList . siteList

edgeKeys :: Mesh -> Set.Set ((Double, Double), (Double, Double))
edgeKeys mesh =
  Set.fromList
    [ if left <= right then (left, right) else (right, left)
    | edge <- undirectedEdges mesh
    , let (from, to) = undirectedEndpoints mesh edge
    , let left = siteKey mesh from
    , let right = siteKey mesh to
    ]

-- | What a mesh looks like when two of them were supposed to be equal.
--
-- The interesting failure is the one where the coordinate-keyed edge sets
-- agree and the values do not: that is the join having become correct only up
-- to DCEL isomorphism, which is precisely what these laws exist to forbid, and
-- a report that only printed counts would hide it.
assertMesh :: String -> Mesh -> Mesh -> IO ()
assertMesh label expected actual
  | expected == actual = pure ()
  | otherwise = fail (label <> ": " <> report)
 where
  report
    | expectedEdges == actualEdges =
        "same geometry, different representation — "
          <> meshCounts expected
          <> " vs "
          <> meshCounts actual
    | otherwise =
        meshCounts expected
          <> " vs "
          <> meshCounts actual
          <> "; edges only in expected: "
          <> show (take 4 (Set.toList (Set.difference expectedEdges actualEdges)))
          <> "; only in actual: "
          <> show (take 4 (Set.toList (Set.difference actualEdges expectedEdges)))
  expectedEdges = edgeKeys expected
  actualEdges = edgeKeys actual

assertMeshEquivalent :: String -> Mesh -> Mesh -> IO ()
assertMeshEquivalent label expected actual = do
  canonicalExpected <-
    requireRight (label <> ": canonical expected") (canonicalize expected)
  canonicalActual <-
    requireRight (label <> ": canonical actual") (canonicalize actual)
  assertMesh label canonicalExpected canonicalActual

meshCounts :: Mesh -> String
meshCounts mesh =
  show (numVertices mesh)
    <> "v/"
    <> show (numUndirectedEdges mesh)
    <> "e/"
    <> show (numFaces mesh)
    <> "f"

-- ── combinatorics and pseudo-randomness ──────────────────────────────────────

pairs :: [a] -> [(a, a)]
pairs values = [(left, right) | left <- values, right <- values]

triples :: [a] -> [(a, a, a)]
triples values = [(a, b, c) | a <- values, b <- values, c <- values]

advance :: Word64 -> Word64
advance state = state * 6364136223846793005 + 1442695040888963407

randomWords :: Word64 -> Int -> [Word64]
randomWords seed count = take count (drop 1 (iterate advance seed))

-- | A deterministic permutation: decorate, sort by the key, discard it.
scramble :: Word64 -> [a] -> [a]
scramble seed values =
  map snd (sortBy (comparing fst) (zip (randomWords seed (length values)) values))

randomSites :: Word64 -> Int -> [Point]
randomSites seed count =
  [ Point (unitCoordinate first) (unitCoordinate second)
  | (first, second) <- take count (chunkPairs (randomWords seed (2 * count)))
  ]

chunkPairs :: [a] -> [(a, a)]
chunkPairs (first : second : rest) = (first, second) : chunkPairs rest
chunkPairs _ = []

unitCoordinate :: Word64 -> Double
unitCoordinate value = 2 * fromIntegral (value `div` 2048) / 9007199254740992 - 1