packages feed

moonlight-planar-1.1.0.0: bench/curve/Moonlight/Planar/CurveBench.hs

-- | New-capability receipts, not a comparison with fixed-sample artwork.
-- Timings include exact leaf-bound observation and complete output forcing.
module Moonlight.Planar.CurveBench (benchmarks) where

import BenchMeasure (requireRight, timedProjection)
import Control.DeepSeq (NFData (..), force)
import Control.Exception (evaluate)
import Data.Foldable (toList, traverse_)
import qualified Data.Sequence as Seq
import Moonlight.Planar.Affine
  ( Affine2, affine2, identityAffine2 )
import Moonlight.Planar.Curve
  ( Subpath (..), CurveStep, ClosedTrail, Located, Path
  , closeWith, closedTrailSteps, cubic, curveStep, ellipse, hermiteStep, line
  , locate, locatedValue, location, transformPath, openTrail, path
  , pathSubpaths, stepControlPoints, trailSteps )
import Moonlight.Planar.Curve.Lowering
  ( LoweredPath, LoweringError, LoweringPolicy, loweredSpans, loweredTolerance
  , lowerClosedTrail, lowerOpenTrail, loweringPolicy, spanFrom, spanParameterFrom
  , spanParameterTo, spanSquaredBound, spanTo )
import Moonlight.Planar.Exact
  ( ExactRational, ExactVector (..), exactPoint, exactPointBitWidth
  , exactRational, exactRationalBitWidth, positiveExact, positiveExactValue )

data CurveFamily
  = HornSpans
  | ExactCircle
  | SkewEllipse
  | RepeatedMotifs
  | AnisotropicHorn
  | CuspsAndOvershoot
  deriving stock (Eq, Show)

data CurveReceipt = CurveReceipt
  { emittedLeaves :: !Int
  , maximumCoordinateBits :: !Int
  , maximumParameterBits :: !Int
  , checkedLeafBounds :: !Int
  , allLeafBoundsPass :: !Bool
  }
  deriving stock (Show)

instance NFData CurveReceipt where
  rnf receipt = rnf (emittedLeaves receipt)
    `seq` rnf (maximumCoordinateBits receipt)
    `seq` rnf (maximumParameterBits receipt)
    `seq` rnf (checkedLeafBounds receipt)
    `seq` rnf (allLeafBoundsPass receipt)

instance Semigroup CurveReceipt where
  a <> b = CurveReceipt
    (emittedLeaves a + emittedLeaves b)
    (max (maximumCoordinateBits a) (maximumCoordinateBits b))
    (max (maximumParameterBits a) (maximumParameterBits b))
    (checkedLeafBounds a + checkedLeafBounds b)
    (allLeafBoundsPass a && allLeafBoundsPass b)

instance Monoid CurveReceipt where
  mempty = CurveReceipt 0 0 0 0 True

benchmarks :: IO ()
benchmarks = do
  putStrLn "curve-benchmark: new capability; no fixed-sample speedup or topology claim"
  putStrLn "curve-benchmark: epsilon=1/8 in declared affine metric; max-depth=16; max-leaves=8192 per subpath"
  putStrLn "curve-benchmark: elapsed includes exact leaf-bound observations and full output forcing; process max-live is cumulative"
  traverse_ benchmarkFamily
    [HornSpans, ExactCircle, SkewEllipse, RepeatedMotifs, AnisotropicHorn, CuspsAndOvershoot]

benchmarkFamily :: CurveFamily -> IO ()
benchmarkFamily family = do
  tolerance <- requireRight (exactRational 1 8) >>= requireRight . positiveExact
  policy <- requireRight (loweringPolicy tolerance (metric family) 16 8192)
  authored <- evaluate (force (geometry family))
  let sourceBits = inputBits authored
      sourceSteps = sum (length . stepsOf <$> toList (pathSubpaths authored))
  result <- timedProjection ("curve-" <> show family) observe
    (requireRight (traverse (lowerSubpath policy) (toList (pathSubpaths authored))))
  let receipt = observe result
  putStrLn ("curve-" <> show family <> "-receipt: input-steps=" <> show sourceSteps
    <> " input-coordinate-bits=" <> show sourceBits
    <> " observed-coordinate-bit-growth=" <> show (max 0 (maximumCoordinateBits receipt - sourceBits))
    <> " " <> show receipt)
  if allLeafBoundsPass receipt
    then pure ()
    else fail ("curve leaf bound failed for " <> show family)

observe :: [LoweredPath] -> CurveReceipt
observe = foldMap observePath

observePath :: LoweredPath -> CurveReceipt
observePath lowered = foldMap observeSpan (loweredSpans lowered)
 where
  epsilon = positiveExactValue (loweredTolerance lowered)
  observeSpan spanValue = CurveReceipt 1
    (max (exactPointBitWidth (spanFrom spanValue)) (exactPointBitWidth (spanTo spanValue)))
    (max (exactRationalBitWidth (spanParameterFrom spanValue))
      (exactRationalBitWidth (spanParameterTo spanValue)))
    1 (spanSquaredBound spanValue >= 0 && spanSquaredBound spanValue <= epsilon * epsilon)

lowerSubpath :: LoweringPolicy -> Subpath -> Either LoweringError LoweredPath
lowerSubpath policy subpath = case subpath of
  OpenSubpath value -> lowerOpenTrail policy value
  ClosedSubpath value -> lowerClosedTrail policy value

inputBits :: Path -> Int
inputBits = foldr max 0 . fmap subpathBits . toList . pathSubpaths
 where
  subpathBits :: Subpath -> Int
  subpathBits subpath = foldr max 0
    (anchorBits subpath : fmap vectorBits (concatMap (toList . stepControlPoints) (stepsOf subpath)))
  anchorBits :: Subpath -> Int
  anchorBits (OpenSubpath value) = exactPointBitWidth (location value)
  anchorBits (ClosedSubpath value) = exactPointBitWidth (location value)
  vectorBits :: ExactVector -> Int
  vectorBits (ExactVector x y) = max (exactRationalBitWidth x) (exactRationalBitWidth y)

stepsOf :: Subpath -> [CurveStep]
stepsOf (OpenSubpath value) = toList (trailSteps (locatedValue value))
stepsOf (ClosedSubpath value) = toList (closedTrailSteps (locatedValue value))

metric :: CurveFamily -> Affine2
metric AnisotropicHorn = affine2 (ExactVector 12 2) (ExactVector 0 1) (ExactVector 0 0)
metric _ = identityAffine2

geometry :: CurveFamily -> Path
geometry HornSpans = closedPath horn
geometry ExactCircle = closedPath (ellipse (ExactVector 40 0) (ExactVector 0 40))
geometry SkewEllipse = closedPath (ellipse (ExactVector 70 15) (ExactVector (-12) 28))
geometry RepeatedMotifs = foldMap motif [0 .. 11]
geometry AnisotropicHorn = closedPath horn
geometry CuspsAndOvershoot = path (Seq.fromList (OpenSubpath . locate (exactPoint 0 0)
  . openTrail . Seq.singleton <$> pathologicalSteps))

closedPath :: Located ClosedTrail -> Path
closedPath = path . Seq.singleton . ClosedSubpath

-- Two Hermite rails share root and tip; changing a middle jet remains local.
horn :: Located ClosedTrail
horn = locate (exactPoint 0 0) (closeWith line (openTrail (Seq.fromList
  [ hermiteStep (ExactVector 28 (-50)) (ExactVector 15 (-65)) (ExactVector 40 (-30))
  , hermiteStep (ExactVector 22 (-30)) (ExactVector 40 (-30)) (ExactVector 8 (-35))
  , hermiteStep (ExactVector (-32) 35) (ExactVector (-45) 10) (ExactVector (-35) 25)
  , hermiteStep (ExactVector (-18) 45) (ExactVector (-35) 25) (ExactVector (-8) 60)
  ])))

motif :: Integer -> Path
motif index = transformPath placement
  (closedPath (ellipse (ExactVector 10 3) (ExactVector (-2) 4)))
 where
  n :: ExactRational
  n = fromInteger index
  placement = affine2 (ExactVector 1 n) (ExactVector 0 1) (ExactVector (25 * n) (3 * n))

pathologicalSteps :: [CurveStep]
pathologicalSteps =
  [ curveStep (cubic (ExactVector 60 80) (ExactVector (-60) 80)) (ExactVector 0 0)
  , curveStep (cubic (ExactVector 100 0) (ExactVector (-100) 0)) (ExactVector 1 0)
  , curveStep (cubic (ExactVector 1 80) (ExactVector (-1) (-80))) (ExactVector 2 0)
  ]