packages feed

moonlight-planar-1.2.0.0: test/curve/Moonlight/Planar/CurveMeasureSpec.hs

-- | Certified arc-length laws against oracles independent of the enclosure
-- kernel: exact integer lengths, exact squaring of irrational ones, a Machin
-- bracket of pi, and a closed-form cusp length. Sampling is never the oracle.
module Moonlight.Planar.CurveMeasureSpec (tests) where

import Data.Foldable (toList, traverse_)
import Data.Ratio (denominator, numerator, (%))
import qualified Data.Sequence as Seq
import Moonlight.Planar.Curve
  ( CurveStep, Located, OpenTrail, Subpath (..), circle, cubic, curveStep, line
  , locate, locatedValue, location, openTrail, quadratic, rationalQuadratic
  , reverseClosedTrail, reverseLocatedTrail )
import Moonlight.Planar.Curve.Measure
import Moonlight.Planar.Exact
  ( ExactPoint, ExactRational, ExactVector (..), exactHalf, exactPoint
  , exactPointCoordinates, exactRational, exactRationalDenominator, exactRationalNumerator
  , positiveExact, positiveOne, positiveTwo, ratioPositive, translateExactPoint, unitHalf
  , unitInterval, unitOne, unitZero )
import Support (assertEqual, requireRight)

tests :: IO ()
tests = sequence_
  [ testPolicies
  , testStraightTrails
  , testStationaryTrails
  , testCircle
  , testCusp
  , testExtremeWeights
  , testBudgets
  , testRanges
  , testBisectionBudget
  , testQueryBudget
  , testSites
  , putStrLn "curve measure: ok"
  ]

testPolicies :: IO ()
testPolicies = do
  tolerance <- exact 1 100 >>= requireRight "tolerance" . positiveExact
  precision <- requireRight "precision" (radicalPrecision 128)
  assertEqual "negative depth refused" (Left (InvalidBudgetDepth (-1))) (() <$ subdivisionBudget (-1) 8 64)
  assertEqual "empty leaf budget refused" (Left (InvalidBudgetLeaves 0)) (() <$ subdivisionBudget 4 0 64)
  assertEqual "empty bit budget refused" (Left (InvalidBudgetBits 0)) (() <$ subdivisionBudget 4 8 0)
  budget <- requireRight "budget" (subdivisionBudget 4 8 64)
  let policy = measurePolicy tolerance precision budget
  assertEqual "policy reads back its parts" (tolerance, precision, budget)
    (measureTolerance policy, measurePrecision policy, measureBudget policy)
  assertEqual "negative distance refused" (Left (NegativeDistance (-1))) (() <$ distance (-1))
  assertEqual "zero distance admitted" (Right 0) (distanceValue <$> distance 0)

testStraightTrails :: IO ()
testStraightTrails = do
  policy <- measuring 1000 24 4096 128 4096
  pythagorean <- measureOpen policy [curveStep line (ExactVector 3 4)]
  assertEqual "a 3-4-5 line has exact length five" (5, 5) (endpoints (lengthBounds pythagorean))

  -- sqrt (1/9 + 1/4) = sqrt 13 / 6, checked by exact squaring.
  third <- exact 1 3
  half <- exact 1 2
  thirteenThirtySixths <- exact 13 36
  irrational <- measureOpen policy [curveStep line (ExactVector third half)]
  let bounds = lengthBounds irrational
  assertEqual "irrational line length is enclosed" True
    (encloses thirteenThirtySixths (lengthEnclosureLower bounds) (lengthEnclosureUpper bounds))
  assertEqual "irrational line width within tolerance" True
    (lengthEnclosureWidth bounds <= toleranceOf 1000)

  let segments = [ExactVector 3 4, ExactVector 6 8]
  unequal <- measureOpen policy (map (curveStep line) segments)
  assertEqual "unequal segments sum exactly" (15, 15) (endpoints (lengthBounds unequal))
  let corner = [ExactVector 4 0, ExactVector 0 3]
  cornered <- measureOpen policy (map (curveStep line) corner)
  assertEqual "a corner sums its legs" (7, 7) (endpoints (lengthBounds cornered))

  start <- sampleAt 0 unequal
  assertEqual "distance zero is the anchor" anchor (sitePoint (sampleSite start))
  checkLinearResidual "distance zero" segments [5, 10] 0 start
  finish <- sampleAt 15 unequal
  assertEqual "the total distance is the end" (exactPoint 11 19) (sitePoint (sampleSite finish))
  assertEqual "the end is exact" 0 (sampleResidual finish)
  middle <- sampleAt 10 unequal
  assertEqual "mid-span sample lies in the second step" 1 (siteStepIndex (sampleSite middle))
  assertEqual "mid-span sample parameter" unitHalf (siteParameter (sampleSite middle))
  assertEqual "mid-span sample point" (exactPoint 8 15) (sitePoint (sampleSite middle))
  checkLinearResidual "mid-span" segments [5, 10] 10 middle
  offCentre <- exact 37 7
  oblique <- sampleAt offCentre unequal
  checkLinearResidual "off-centre" segments [5, 10] offCentre oblique
  atCorner <- sampleAt 4 cornered
  assertEqual "a corner distance is the corner" (exactPoint 6 7) (sitePoint (sampleSite atCorner))
  checkLinearResidual "corner" corner [4, 3] 4 atCorner
  total <- requireRight "total" (distance 16)
  assertEqual "a distance beyond the total is refused"
    (Left (DistanceBeyondTrail 16 (lengthBounds unequal))) (() <$ pointAtLength total unequal)

  reversed <- requireRight "reversed" (measureSubpath policy
    (OpenSubpath (reverseLocatedTrail (locate anchor (openTrail (Seq.fromList (map (curveStep line) corner)))))))
  assertEqual "reversal keeps the length bounds" (lengthBounds cornered) (lengthBounds reversed)

testStationaryTrails :: IO ()
testStationaryTrails = do
  policy <- measuring 1000 24 4096 128 4096
  stationary <- measureOpen policy
    [curveStep line zero, curveStep (cubic zero zero) zero, curveStep (quadratic zero) zero]
  assertEqual "a stationary trail has zero length" (0, 0) (endpoints (lengthBounds stationary))
  still <- sampleAt 0 stationary
  assertEqual "a stationary trail samples its anchor" (anchor, 0) (sitePoint (sampleSite still), sampleResidual still)
  beyond <- requireRight "beyond" (distance 1)
  assertEqual "a stationary trail refuses positive distance"
    (Left (DistanceBeyondTrail 1 (lengthBounds stationary))) (() <$ pointAtLength beyond stationary)
  halfway <- requireRight "halfway" (pointAtFraction unitHalf stationary)
  assertEqual "a stationary fraction is the anchor" anchor (sitePoint (sampleSite halfway))

  padded <- measureOpen policy
    [curveStep line zero, curveStep line (ExactVector 3 4), curveStep line zero]
  assertEqual "stationary spans add nothing" (5, 5) (endpoints (lengthBounds padded))
  arrival <- sampleAt 5 padded
  assertEqual "the full distance reaches the displaced point" (exactPoint 5 11) (sitePoint (sampleSite arrival))

  empty <- requireRight "empty" (measureSubpath policy (OpenSubpath (locate anchor (mempty :: OpenTrail))))
  assertEqual "an empty trail has zero length" (0, 0) (endpoints (lengthBounds empty))
  origin <- requireRight "origin" (distance 0)
  assertEqual "an empty trail has no source span to sample"
    (Left EmptyTrailSample) (() <$ pointAtLength origin empty)

testCircle :: IO ()
testCircle = do
  let (piLow, piHigh) = piBracket
      source = ClosedSubpath (circle positiveTwo)
      west = exactPoint (-2) 0
  fine <- measuring 1000 32 8192 128 8192
  coarse <- measuring 100 32 8192 128 8192
  measured <- requireRight "circle" (measureSubpath fine source)
  rough <- requireRight "coarse circle" (measureSubpath coarse source)
  let bounds = lengthBounds measured
  assertEqual "the measured trail retains its source" source (measuredSource measured)
  assertEqual "circumference 4 pi is enclosed" True
    (rational (lengthEnclosureLower bounds) <= 4 * piLow && 4 * piHigh <= rational (lengthEnclosureUpper bounds))
  assertEqual "circle width within tolerance" True (lengthEnclosureWidth bounds <= toleranceOf 1000)
  assertEqual "summed enclosures stay on the precision's dyadic grid" True
    (all (\value -> (2 ^ (128 :: Int)) `mod` exactRationalDenominator value == 0)
      [lengthEnclosureLower bounds, lengthEnclosureUpper bounds])
  assertEqual "a tighter tolerance nests its bounds" True
    (lengthEnclosureLower (lengthBounds rough) <= lengthEnclosureLower bounds
      && lengthEnclosureUpper bounds <= lengthEnclosureUpper (lengthBounds rough))
  assertEqual "spans are ordered cumulative enclosures" True
    (and (zipWith (\before after -> lengthEnclosureUpper (measuredSpanPrefix before) <= lengthEnclosureUpper (measuredSpanPrefix after))
      (toList (measuredSpans measured)) (drop 1 (toList (measuredSpans measured)))))

  reversed <- requireRight "reversed circle"
    (measureSubpath fine (ClosedSubpath (locate (location (circle positiveTwo))
      (reverseClosedTrail (locatedValue (circle positiveTwo))))))
  assertEqual "reversal keeps the circle's bounds" bounds (lengthBounds reversed)

  -- The half circumference 2 pi ends at (-2, 0); a chord is no longer than
  -- the arc it spans, so the sample's chord to (-2, 0) is bounded by its
  -- residual plus the request's distance from 2 pi.
  halfCircumference <- fromRatio (2 * piLow)
  opposite <- sampleAt halfCircumference measured
  assertEqual "a length sample lies on the circle" 4 (squaredNorm (sitePoint (sampleSite opposite)))
  assertEqual "a length sample is within its residual of the half circumference" True
    (squaredDistance west (sitePoint (sampleSite opposite))
      <= square (rational (sampleResidual opposite) + 2 * (piHigh - piLow)))
  assertEqual "a length residual is within tolerance" True (sampleResidual opposite <= toleranceOf 1000)
  midway <- requireRight "half fraction" (pointAtFraction unitHalf measured)
  assertEqual "a fraction sample lies on the circle" 4 (squaredNorm (sitePoint (sampleSite midway)))
  assertEqual "the half fraction is within its residual of the opposite point" True
    (squaredDistance west (sitePoint (sampleSite midway)) <= square (rational (sampleResidual midway)))
  seam <- requireRight "seam" (pointAtFraction unitZero measured)
  assertEqual "fraction zero is the seam" (exactPoint 2 0) (sitePoint (sampleSite seam))
  lap <- requireRight "lap" (pointAtFraction unitOne measured)
  assertEqual "fraction one closes the lap within its residual" True
    (squaredDistance (exactPoint 2 0) (sitePoint (sampleSite lap)) <= square (rational (sampleResidual lap)))
  quarter <- exact 1 4 >>= requireRight "quarter fraction" . unitInterval
  quarterSample <- requireRight "quarter sample" (pointAtFraction quarter measured)
  assertEqual "the quarter fraction is within its residual of the north point" True
    (squaredDistance (exactPoint 0 2) (sitePoint (sampleSite quarterSample)) <= square (rational (sampleResidual quarterSample)))

testCusp :: IO ()
testCusp = do
  -- x' = 3(1-2t)^2 and y' = 3(1-2t): a cusp at t = 1/2, of length 2 sqrt 2 - 1.
  policy <- measuring 1000 32 8192 128 8192
  measured <- measureOpen policy [curveStep (cubic (ExactVector 1 1) (ExactVector 0 1)) (ExactVector 1 0)]
  let bounds = lengthBounds measured
      shifted value = rational value + 1
  assertEqual "the cusp length 2 sqrt 2 - 1 is enclosed" True
    (square (shifted (lengthEnclosureLower bounds)) <= 8 && 8 <= square (shifted (lengthEnclosureUpper bounds)))
  assertEqual "cusp width within tolerance" True (lengthEnclosureWidth bounds <= toleranceOf 1000)
  cusp <- sampleAt (lengthEnclosureLower bounds * exactHalf) measured
  assertEqual "a cusp sample residual is within tolerance" True (sampleResidual cusp <= toleranceOf 1000)

  -- x = 3t(1-t)(1-2t) on the axis turns at t = (3 -+ sqrt 3)/6, where x is
  -- +-sqrt 3 / 6, so the arc is 2 sqrt 3 / 3 and its square is 4/3. A span
  -- straddling an irrational turning point never meets a purely relative
  -- allowance; the parameter half of the budget accepts it.
  reversing <- measureOpen policy [curveStep (cubic (ExactVector 1 0) (ExactVector (-1) 0)) zero]
  let turning = lengthBounds reversing
  fourThirds <- exact 4 3
  assertEqual "a reversal at irrational parameters is enclosed" True
    (encloses fourThirds (lengthEnclosureLower turning) (lengthEnclosureUpper turning))
  assertEqual "reversal width within tolerance" True (lengthEnclosureWidth turning <= toleranceOf 1000)
  midway <- sampleAt (lengthEnclosureLower turning * exactHalf) reversing
  assertEqual "a reversal sample residual is within tolerance" True (sampleResidual midway <= toleranceOf 1000)

testExtremeWeights :: IO ()
testExtremeWeights = do
  policy <- measuring 1000 64 16384 128 16384
  large <- exact 1000 1 >>= requireRight "large weight" . positiveExact
  huge <- exact 1000000 1 >>= requireRight "huge weight" . positiveExact
  let small = ratioPositive positiveOne large
      control = ExactVector 5 (-3)
      end = ExactVector 2 7
      conic u v = curveStep (rationalQuadratic control u v) end
  traverse_ (\(label, step) -> do
    measured <- measureOpen policy [step]
    reversed <- requireRight label (measureSubpath policy
      (OpenSubpath (reverseLocatedTrail (locate anchor (openTrail (Seq.singleton step))))))
    let bounds = lengthBounds measured
    assertEqual (label <> " width within tolerance") True (lengthEnclosureWidth bounds <= toleranceOf 1000)
    assertEqual (label <> " reversal keeps the bounds") bounds (lengthBounds reversed))
    [ ("weights 1/1000 then 1000", conic small large)
    , ("weights 1000 then 1/1000", conic large small)
    , ("weights 1000 then 10^6", conic large huge)
    ]
  -- Weights (1, k, k^2) reparameterize the polynomial quadratic: one arc,
  -- so the two independently certified enclosures must intersect.
  polynomial <- measureOpen policy [curveStep (quadratic control) end]
  reparameterized <- measureOpen policy [conic large huge]
  let a = lengthBounds polynomial
      b = lengthBounds reparameterized
  assertEqual "a reparameterized conic has the polynomial's length" True
    (max (lengthEnclosureLower a) (lengthEnclosureLower b) <= min (lengthEnclosureUpper a) (lengthEnclosureUpper b))

testBudgets :: IO ()
testBudgets = do
  let source = ClosedSubpath (circle positiveTwo)
  leafy <- measuring 1000 32 4 128 8192
  assertEqual "leaf budget exhaustion refuses" (Just (MeasureBudgetExhausted LeavesExhausted))
    (obligation (measureSubpath leafy source))
  shallow <- measuring 1000 1 8192 128 8192
  assertEqual "depth budget exhaustion refuses" (Just (MeasureBudgetExhausted DepthExhausted))
    (obligation (measureSubpath shallow source))
  narrow <- measuring 1000 32 8192 128 8
  assertEqual "bit budget exhaustion refuses" True
    (case obligation (measureSubpath narrow source) of
      Just (MeasureBudgetExhausted (BitsExhausted width)) -> width > 8
      _ -> False)
  -- At one bit, sqrt 2 is enclosed by [1, 3/2]: the chord and polygon widths
  -- of 1/2 each exceed the span's allowance of (1/200)(1/(3/2) + 1) = 1/120,
  -- while a zero-width comparison of the identical chord and polygon would pass.
  coarse <- measuring 100 32 8192 1 8192
  share <- exact 1 120
  assertEqual "rounding beyond the allowance refuses as precision"
    (Left (SpanRefused 0 unitZero unitOne (PrecisionExhausted 1 share)))
    (() <$ measureSubpath coarse (OpenSubpath (locate anchor (openTrail (Seq.singleton (curveStep line (ExactVector 1 1)))))))

  -- Tiny coordinates do not admit an oversized weight: the source is refused
  -- at the offending step before any length is observed.
  huge <- requireRight "huge weight" (positiveExact (2 ^ (400 :: Int)))
  half <- exact 1 2
  small <- measuring 1000 32 8192 128 64
  let tiny = ExactVector half half
      weighted = OpenSubpath (located
        [curveStep line tiny, curveStep (rationalQuadratic tiny huge positiveOne) tiny])
  assertEqual "an oversized weight refuses before measurement" True
    (case measureSubpath small weighted of
      Left (SpanRefused 1 from to (MeasureBudgetExhausted (BitsExhausted width))) -> (from, to) == (unitZero, unitOne) && width > 64
      _ -> False)
  -- The same budget admits the line at 128 bits of radical precision but not
  -- at 4096, whose dyadic enclosure endpoints alone exceed it.
  let diagonal = OpenSubpath (located [curveStep line (ExactVector 1 1)])
  modest <- measuring 1000 32 8192 128 512
  _ <- requireRight "modest precision" (measureSubpath modest diagonal)
  lavish <- measuring 1000 32 8192 4096 512
  assertEqual "precision beyond the bit budget refuses" True
    (case measureSubpath lavish diagonal of
      Left (SpanRefused 0 from to (MeasureBudgetExhausted (BitsExhausted width))) -> (from, to) == (unitZero, unitOne) && width > 512
      _ -> False)

-- A two-bit budget admits the unit line, the request 1/2 and the halves, but
-- not the quarter parameters; bisecting toward 1/2 refuses the child
-- [1/2, 3/4] before observing it rather than answering with unadmitted bits.
testBisectionBudget :: IO ()
testBisectionBudget = do
  policy <- measuring 4 32 8192 128 2
  measured <- requireRight "unit line" (measureSubpath policy
    (OpenSubpath (locate (exactPoint 0 0) (openTrail (Seq.singleton (curveStep line (ExactVector 1 0)))))))
  half <- exact 1 2
  threeQuarters <- exact 3 4 >>= requireRight "three quarters" . unitInterval
  target <- requireRight "distance" (distance half)
  assertEqual "a bisection child beyond the bit budget refuses"
    (Left (SpanRefused 0 unitHalf threeQuarters (MeasureBudgetExhausted (BitsExhausted 3)))) (() <$ pointAtLength target measured)

-- An exact unit root stays small at 128 bits of precision, so a 16-bit
-- budget admits the line; a request whose own denominator exceeds that
-- budget is refused before any span is consulted.
testQueryBudget :: IO ()
testQueryBudget = do
  policy <- measuring 4 32 8192 128 16
  measured <- requireRight "unit line" (measureSubpath policy
    (OpenSubpath (locate (exactPoint 0 0) (openTrail (Seq.singleton (curveStep line (ExactVector 1 0)))))))
  assertEqual "an exact root at high precision is admitted under a small budget"
    (1, 1) (endpoints (lengthBounds measured))
  target <- exact 1 (2 ^ (1000 :: Int)) >>= requireRight "distance" . distance
  assertEqual "a request beyond the bit budget is refused at admission"
    (Left (RequestRefused (MeasureBudgetExhausted (BitsExhausted 1001)))) (() <$ pointAtLength target measured)

-- The irrational diagonal's rational upper bound lies strictly beyond sqrt 2,
-- so requesting it is undecided, never answered by the trail's end; its lower
-- bound is answered. A fraction reserves its share of the width first.
testRanges :: IO ()
testRanges = do
  policy <- measuring 1000 32 8192 128 4096
  let diagonal = OpenSubpath (located [curveStep line (ExactVector 1 1)])
  measured <- requireRight "diagonal" (measureSubpath policy diagonal)
  let bounds = lengthBounds measured
  assertEqual "the diagonal's enclosure is not exact" True
    (lengthEnclosureLower bounds < lengthEnclosureUpper bounds)
  upper <- requireRight "upper distance" (distance (lengthEnclosureUpper bounds))
  assertEqual "the upper endpoint of an inexact length is unresolved"
    (Left (DistanceUnresolved (lengthEnclosureUpper bounds) bounds)) (() <$ pointAtLength upper measured)
  lower <- sampleAt (lengthEnclosureLower bounds) measured
  assertEqual "the lower endpoint is answered within tolerance" True (sampleResidual lower <= toleranceOf 1000)

  -- Tolerance one at one bit: the enclosure [1, 3/2] leaves a quarter of
  -- uncertainty at the half fraction, reserved before the inverse runs.
  loose <- measuring 1 32 8192 1 4096
  coarse <- requireRight "coarse diagonal" (measureSubpath loose diagonal)
  threeHalves <- exact 3 2
  assertEqual "one bit encloses the diagonal in [1, 3/2]" (1, threeHalves) (endpoints (lengthBounds coarse))
  half <- requireRight "half fraction" (pointAtFraction unitHalf coarse)
  threeQuarters <- exact 3 4
  assertEqual "the inverse runs at 3/4 and the reserve of 1/4 widens it"
    (threeQuarters, unitZero, unitHalf) (sampleResidual half, sampleParameterFrom half, sampleParameterTo half)
  assertEqual "the reserved fraction residual is within tolerance" True (sampleResidual half <= 1)

-- A sample retains its source and names the step it lies on, that step's
-- located start, and its side of any join.
testSites :: IO ()
testSites = do
  policy <- measuring 1000 24 4096 128 4096
  let source = OpenSubpath (located (map (curveStep line) [ExactVector 3 4, ExactVector 6 8]))
  measured <- requireRight "unequal lines" (measureSubpath policy source)
  join <- sampleAt 5 measured
  assertEqual "a sample retains its source" source (siteSource (sampleSite join))
  traverse_ (\value -> sampleAt value measured >>= \sample ->
    assertEqual "every sample's source is its trail's" (measuredSource measured) (siteSource (sampleSite sample)))
    [0, 5, 10, 15]
  assertEqual "the join distance lies after the join"
    (1, AfterJoin, exactPoint 5 11)
    (siteStepIndex (sampleSite join), siteJoinSide (sampleSite join), sitePoint (sampleSite join))
  start <- sampleAt 0 measured
  assertEqual "an open trail's start is away from any join" AwayFromJoin (siteJoinSide (sampleSite start))
  finish <- sampleAt 15 measured
  assertEqual "an open trail's end is away from any join"
    (1, unitOne, AwayFromJoin)
    (siteStepIndex (sampleSite finish), siteParameter (sampleSite finish), siteJoinSide (sampleSite finish))
  lap <- requireRight "circle" (measureSubpath policy (ClosedSubpath (circle positiveTwo)))
  seam <- requireRight "seam" (pointAtFraction unitZero lap)
  assertEqual "a closed trail's seam is a join" (0, AfterJoin) (siteStepIndex (sampleSite seam), siteJoinSide (sampleSite seam))

-- Straight-trail residual law: the arc distance of a point on step @i@ is the
-- rational length before it plus the exact distance from that step's start.
checkLinearResidual :: String -> [ExactVector] -> [ExactRational] -> ExactRational -> ArcSample -> IO ()
checkLinearResidual label segments lengths target sample =
  let index = siteStepIndex (sampleSite sample)
      before = sum (take index lengths)
      stepStart = foldl translateExactPoint anchor (take index segments)
      reach = squaredDistance stepStart (sitePoint (sampleSite sample))
      residual = rational (sampleResidual sample)
      low = rational (target - before) - residual
      high = rational (target - before) + residual
   in do
        assertEqual (label <> " residual within tolerance") True (sampleResidual sample <= toleranceOf 1000)
        assertEqual (label <> " arc distance within residual") True
          ((low <= 0 || square low <= reach) && high >= 0 && reach <= square high)

-- Machin's formula over alternating arctangent series: consecutive partial
-- sums of a series with decreasing terms bracket its limit.
piBracket :: (Rational, Rational)
piBracket = (16 * low5 - 4 * high239, 16 * high5 - 4 * low239)
 where
  (low5, high5) = arctangent (1 % 5)
  (low239, high239) = arctangent (1 % 239)
  arctangent :: Rational -> (Rational, Rational)
  arctangent x =
    let partial n = sum [(-1) ^ k * x ^ (2 * k + 1) / fromInteger (2 * k + 1) | k <- [0 .. n - 1 :: Integer]]
     in (min (partial 12) (partial 13), max (partial 12) (partial 13))

measuring :: Integer -> Int -> Int -> Int -> Int -> IO MeasurePolicy
measuring reciprocal depth leaves bits width = do
  tolerance <- exact 1 reciprocal >>= requireRight "tolerance" . positiveExact
  precision <- requireRight "precision" (radicalPrecision bits)
  budget <- requireRight "measure budget" (subdivisionBudget depth leaves width)
  pure (measurePolicy tolerance precision budget)

measureOpen :: MeasurePolicy -> [CurveStep] -> IO MeasuredTrail
measureOpen policy stepsValue = requireRight "measured trail"
  (measureSubpath policy (OpenSubpath (located stepsValue)))

located :: [CurveStep] -> Located OpenTrail
located = locate anchor . openTrail . Seq.fromList

sampleAt :: ExactRational -> MeasuredTrail -> IO ArcSample
sampleAt value trail = requireRight "distance" (distance value)
  >>= \target -> requireRight "length sample" (pointAtLength target trail)

obligation :: Either MeasureError a -> Maybe MeasureObligation
obligation (Left (SpanRefused _ _ _ refused)) = Just refused
obligation _ = Nothing

endpoints :: LengthEnclosure -> (ExactRational, ExactRational)
endpoints bounds = (lengthEnclosureLower bounds, lengthEnclosureUpper bounds)

encloses :: ExactRational -> ExactRational -> ExactRational -> Bool
encloses squared lower upper = lower <= upper && lower * lower <= squared && squared <= upper * upper

anchor :: ExactPoint
anchor = exactPoint 2 7

zero :: ExactVector
zero = ExactVector 0 0

exact :: Integer -> Integer -> IO ExactRational
exact n d = requireRight "exact rational" (exactRational n d)

fromRatio :: Rational -> IO ExactRational
fromRatio value = exact (numerator value) (denominator value)

toleranceOf :: Integer -> ExactRational
toleranceOf reciprocal = either (const 0) id (exactRational 1 reciprocal)

rational :: ExactRational -> Rational
rational value = exactRationalNumerator value % exactRationalDenominator value

square :: Rational -> Rational
square value = value * value

squaredNorm :: ExactPoint -> ExactRational
squaredNorm point = let (x, y) = exactPointCoordinates point in x * x + y * y

squaredDistance :: ExactPoint -> ExactPoint -> Rational
squaredDistance a b =
  let (ax, ay) = exactPointCoordinates a
      (bx, by) = exactPointCoordinates b
   in square (rational (bx - ax)) + square (rational (by - ay))