moonlight-planar-1.2.0.0: src-dcel/Moonlight/Planar/Curve/Proximity.hs
-- | Bounded distance and clearance between two curves, in the coordinates of
-- the submitted curves. Distance and contact are separate obligations: a
-- distance enclosure never implies contact, and contact is certified only by
-- an exact common point or a certified crossing. Proximity certifies "within
-- a distance", never "exactly on", except at evaluated rational sites.
--
-- The search is best-first over pairs of source spans, one from each curve,
-- in exact rational arithmetic. A pair's lower bound is the squared
-- distance between its pieces' control hulls, since a positive-weight piece
-- lies in the convex hull of its controls. The upper bound is the
-- least squared distance between evaluated sites: every piece's endpoints,
-- whose points are those of exact subdivision, and on a straight piece the
-- nearest point to the other piece's endpoints, at its rational parameter. A
-- pair whose lower bound exceeds the upper bound is dropped, since the
-- minimum does not lie there; a pair of two stationary pieces is exact and
-- kept only as sites. The pair with the least lower bound is refined by
-- halving its larger piece. The one square root is taken when an enclosure
-- is reported.
--
-- The pairs left live are candidates: several stay several, and none is
-- claimed to hold a unique nearest point. The subdivision budget bounds
-- depth below any source step and the pairs evaluated in all, and its bits
-- bound every exact value retained or compared, each admitted before it is:
-- the threshold and its square; every span, located controls included, so
-- every endpoint site; every projected site, site displacement and its
-- square, hull gap and its square; every crossing certificate, admitted
-- where it is made; and both ends of every root enclosure, as Measure admits
-- its enclosures. A projected site, being optional, is dropped when too wide;
-- anything else too wide refuses. The radical precision bounds only the
-- reported enclosure.
module Moonlight.Planar.Curve.Proximity
( CurveSpan
, curveSpanSource
, curveSpanStep
, curveSpanFrom
, curveSpanTo
, ProximityCandidate
, candidateFirst
, candidateSecond
, DistanceObservation
, distanceBounds
, distanceWitness
, distanceCandidates
, ProximityObligation (..)
, ProximityError (..)
, curveDistance
, ContactWitness (..)
, ClearanceVerdict (..)
, clearance
) where
import Control.DeepSeq (NFData (..))
import Control.Monad (foldM)
import Data.Either (isRight)
import Data.Foldable (toList, traverse_)
import qualified Data.List.NonEmpty as NonEmpty
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Map.Strict as Map
import Data.Map.Strict (Map)
import Data.Maybe (mapMaybe)
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Moonlight.Planar.Curve
( CurveShapeView (..), Subpath (..), curveStepEnd, curveStepShape, shapeView )
import Moonlight.Planar.Curve.Measure
( Distance, MeasurePolicy, distanceValue, measureBudget, measurePrecision, measureTolerance )
import Moonlight.Planar.Exact
( ExactBounds, ExactPoint, ExactRational, ExactVector (..), UnitInterval, boundsMaximumX
, boundsMaximumY, boundsMinimumX, boundsMinimumY, exactPointBitWidth, exactPointCoordinates
, exactPointsBounds, exactRationalBitWidth, positiveExact, positiveExactValue, translateExactPoint
, unitIntervalValue )
import Moonlight.Planar.Internal.CurveBudget
( BudgetObligation (..), SubdivisionBudget, budgetBits, budgetDepth, budgetLeaves )
import Moonlight.Planar.Internal.CurveCertificate
( CrossingCertificate, CrossingVerdict (..), crossingVerdict, hullGap, stationaryPiece )
import Moonlight.Planar.Internal.CurveSource
( SourceSpan, TrailSite, admitSpan, halveSpan, sitePoint, sourceSpanControls, sourceSpanFrom, sourceSpanPiece
, sourceSpanStart, sourceSpanStep, sourceSpanTo, sourceStepIndex, sourceSteps, trailSite, wholeSpan )
import Moonlight.Planar.Internal.ExactRational (divideByPositive, unitBetween, unitClamp)
import Moonlight.Planar.Internal.Length
( LengthEnclosure, RadicalPrecision, enclosureBetween, euclideanLengthEnclosure
, lengthEnclosureLower, lengthEnclosureUpper, lengthEnclosureWidth )
-- | A bracket of one source step of a curve: the source, the step's index
-- among its steps ('closedTrailSteps' order for a closed trail), and the
-- bracket in that step's parameter.
data CurveSpan = CurveSpan !Subpath !SourceSpan
deriving stock (Eq, Show)
instance NFData CurveSpan where
rnf (CurveSpan source span') = rnf source `seq` rnf span'
curveSpanSource :: CurveSpan -> Subpath
curveSpanSource (CurveSpan source _) = source
curveSpanStep :: CurveSpan -> Int
curveSpanStep (CurveSpan _ span') = sourceStepIndex (sourceSpanStep span')
curveSpanFrom :: CurveSpan -> UnitInterval
curveSpanFrom (CurveSpan _ span') = sourceSpanFrom span'
curveSpanTo :: CurveSpan -> UnitInterval
curveSpanTo (CurveSpan _ span') = sourceSpanTo span'
-- | A live pair of spans, one on each curve, whose lower bound does not
-- exceed the distance found: the minimum may lie there.
data ProximityCandidate = ProximityCandidate !CurveSpan !CurveSpan
deriving stock (Eq, Show)
instance NFData ProximityCandidate where
rnf (ProximityCandidate first second) = rnf first `seq` rnf second
candidateFirst :: ProximityCandidate -> CurveSpan
candidateFirst (ProximityCandidate first _) = first
candidateSecond :: ProximityCandidate -> CurveSpan
candidateSecond (ProximityCandidate _ second) = second
-- | The distance between the curves lies in the enclosure, whose width is at
-- most the policy tolerance; the two sites, one on each curve, are as far
-- apart as its upper end allows.
data DistanceObservation = DistanceObservation !LengthEnclosure !TrailSite !TrailSite !(Seq ProximityCandidate)
deriving stock (Eq, Show)
instance NFData DistanceObservation where
rnf (DistanceObservation enclosure first second candidates) =
rnf enclosure `seq` rnf first `seq` rnf second `seq` rnf candidates
distanceBounds :: DistanceObservation -> LengthEnclosure
distanceBounds (DistanceObservation enclosure _ _ _) = enclosure
distanceWitness :: DistanceObservation -> (TrailSite, TrailSite)
distanceWitness (DistanceObservation _ first second _) = (first, second)
distanceCandidates :: DistanceObservation -> Seq ProximityCandidate
distanceCandidates (DistanceObservation _ _ _ candidates) = candidates
-- | Why a distance was not enclosed within the tolerance: the subdivision
-- budget, or, carrying the enclosure's rounding and the tolerance, a radical
-- precision too coarse for the tolerance however far the search refines.
data ProximityObligation
= ProximityBudgetExhausted !BudgetObligation
| ProximityPrecisionExhausted !ExactRational !ExactRational
deriving stock (Eq, Show)
-- | A curve with no steps has no span to measure from. A clearance threshold
-- or its square wider than the bit budget is refused on entry. Step pairs,
-- first curve's steps by second's, more than the leaf budget are refused
-- from the two counts alone, before any source is admitted or pair built. A
-- source step whose start, controls or weights, relative or located, exceed
-- the bit budget is refused before any pair is evaluated. A value wider than the bit budget
-- met before any enclosure is admitted, or in the enclosure a result would
-- report, is refused with no enclosure. Otherwise a refused distance carries
-- the last enclosure admitted and the live candidates there.
data ProximityError
= EmptyProximitySource
| ProximityRequestRefused !ProximityObligation
| ProximitySourceRefused !CurveSpan !BudgetObligation
| ProximityStepPairsRefused !Int !Int
| ProximityUnenclosed !ProximityObligation
| ProximityRefused !ProximityObligation !LengthEnclosure !(Seq ProximityCandidate)
deriving stock (Eq, Show)
-- | Why clearance fails: two evaluated sites no farther apart than the
-- threshold, one on each curve; one exact point on both curves; or a
-- certified single transversal crossing of two spans, whose point, algebraic
-- in general, is not given.
data ContactWitness
= CloserThan !TrailSite !TrailSite
| SharedPoint !TrailSite !TrailSite
| TransversalCrossing !CurveSpan !CurveSpan !CrossingCertificate
deriving stock (Eq, Show)
instance NFData ContactWitness where
rnf (CloserThan first second) = rnf first `seq` rnf second
rnf (SharedPoint first second) = rnf first `seq` rnf second
rnf (TransversalCrossing first second certificate) = rnf first `seq` rnf second `seq` rnf certificate
-- | Clearance at a threshold @d@ is strict: it holds when the distance
-- exceeds @d@, so at zero it holds exactly when the curves are disjoint. It
-- is violated, with a witness, when the distance is at most @d@. Otherwise
-- the budget ran out first, with the enclosure and candidates reached; a
-- tangency at a parameter subdivision never reaches, or a distance of
-- exactly @d@ approached from above, never resolves. Enclosures are reported
-- at the policy's precision and are not bound by its tolerance.
data ClearanceVerdict
= ClearanceHolds !LengthEnclosure
| ClearanceViolated !ContactWitness
| ClearanceUnresolved !LengthEnclosure !(Seq ProximityCandidate) !BudgetObligation
deriving stock (Eq, Show)
instance NFData ClearanceVerdict where
rnf (ClearanceHolds enclosure) = rnf enclosure
rnf (ClearanceViolated witness) = rnf witness
rnf (ClearanceUnresolved enclosure candidates obligation) =
rnf enclosure `seq` rnf candidates `seq` rnf obligation
-- | The distance between two curves within the policy tolerance. Exhaustion
-- refuses; a success is never wider than the tolerance.
curveDistance :: MeasurePolicy -> Subpath -> Subpath -> Either ProximityError DistanceObservation
curveDistance policy first second = startSearch False budget first second >>= search Nothing
where
budget = measureBudget policy
precision = measurePrecision policy
tolerance = positiveExactValue (measureTolerance policy)
-- The last admitted enclosure and its candidates, when there is one.
search
:: Maybe (LengthEnclosure, Seq ProximityCandidate) -> Search -> Either ProximityError DistanceObservation
search admitted reached@(Search frontier (Nearest _ _ siteA siteB) _ _ _ _) =
case enclosed budget precision reached of
Left obligation -> Left (refusal (ProximityBudgetExhausted obligation) admitted)
Right (lower, upper)
| lengthEnclosureWidth reached' <= tolerance -> Right (DistanceObservation reached' siteA siteB candidates)
| otherwise -> case Map.minViewWithKey frontier of
-- Refinement narrows a geometric gap; once rounding alone exceeds
-- the tolerance, or the bounds are exact, it cannot help.
Just ((_, (_, pair)), rest)
| optimisticGap > tolerance || rounding <= tolerance ->
either
(\(obligation, _) -> Left (refusal (ProximityBudgetExhausted obligation) here))
(search here)
(refine budget reached rest pair)
_ -> Left (refusal (ProximityPrecisionExhausted rounding tolerance) here)
where
reached' = enclosureBetween lower upper
candidates = liveCandidates reached
here = Just (reached', candidates)
optimisticGap = lengthEnclosureLower upper - lengthEnclosureUpper lower
rounding = lengthEnclosureWidth lower + lengthEnclosureWidth upper
refusal :: ProximityObligation -> Maybe (LengthEnclosure, Seq ProximityCandidate) -> ProximityError
refusal obligation = maybe (ProximityUnenclosed obligation) (uncurry (ProximityRefused obligation))
-- | Strict clearance at the threshold between two curves.
clearance :: MeasurePolicy -> Distance -> Subpath -> Subpath -> Either ProximityError ClearanceVerdict
clearance policy threshold first second = do
-- The threshold before its square, so an over-wide threshold is never squared.
either (Left . ProximityRequestRefused . ProximityBudgetExhausted) Right
(admitWidth budget (exactRationalBitWidth level) *> admitWidth budget (exactRationalBitWidth limit))
startSearch True budget first second >>= decide
where
budget = measureBudget policy
precision = measurePrecision policy
level = distanceValue threshold
limit = level * level
decide :: Search -> Either ProximityError ClearanceVerdict
decide reached@(Search frontier (Nearest squared _ siteA siteB) crossing _ _ _)
| squared == 0 = Right (ClearanceViolated (SharedPoint siteA siteB))
| squared <= limit = Right (ClearanceViolated (CloserThan siteA siteB))
| Just (spanA, spanB, certificate) <- crossing = Right (ClearanceViolated (TransversalCrossing spanA spanB certificate))
| otherwise = case Map.minViewWithKey frontier of
Just (((gap, _), (_, pair)), rest) | gap <= limit ->
either
(\(obligation, _) -> (\enclosure -> ClearanceUnresolved enclosure (liveCandidates reached) obligation) <$> report reached)
decide (refine budget reached rest pair)
-- No live pair lies within the threshold.
_ -> ClearanceHolds <$> report reached
report :: Search -> Either ProximityError LengthEnclosure
report reached =
either (Left . ProximityUnenclosed . ProximityBudgetExhausted) (Right . uncurry enclosureBetween)
(enclosed budget precision reached)
-- A piece of one curve: its depth below its source step, its source, its
-- span, and its control box, whose larger side picks the piece to halve.
data Piece = Piece !Int !Subpath !SourceSpan !ExactBounds
data Pair = Pair !Piece !Piece
-- A pair of evaluated sites, one on each curve, with their squared distance
-- and displacement.
data Nearest = Nearest !ExactRational !ExactVector !TrailSite !TrailSite
-- The live pairs keyed by squared gap and admission ordinal, each with its
-- gap displacement; the nearest sites; the first certified crossing, when
-- crossings are watched; the pairs evaluated; and whether crossings are
-- watched.
data Search = Search
!(Map (ExactRational, Int) (ExactVector, Pair)) !Nearest
!(Maybe (CurveSpan, CurveSpan, CrossingCertificate)) !Int !Int !Bool
-- Every step pair, evaluated in order. The pair count is decided from the
-- two step counts, their exact product against the leaf budget, before any
-- source is admitted or any pair is built; an empty curve's product is zero.
startSearch :: Bool -> SubdivisionBudget -> Subpath -> Subpath -> Either ProximityError Search
startSearch watching budget first second
| toInteger countA * toInteger countB > toInteger (budgetLeaves budget) =
Left (ProximityStepPairsRefused countA countB)
| otherwise = do
piecesA <- traverse (admitSource budget) (sourcePieces first)
piecesB <- traverse (admitSource budget) (sourcePieces second)
case (piecesA, piecesB) of
(a : _, b : _) -> either (Left . ProximityUnenclosed . ProximityBudgetExhausted) Right $ do
nearest :| _ <- pairSites budget (Pair a b)
foldM (enter budget) (Search Map.empty nearest Nothing 0 0 watching)
[Pair pieceA pieceB | pieceA <- piecesA, pieceB <- piecesB]
_ -> Left EmptyProximitySource
where
countA = Seq.length (sourceSteps first)
countB = Seq.length (sourceSteps second)
-- Halve the pair's larger refinable piece and admit both children. A refusal
-- keeps the search as it was, the pair still live.
refine
:: SubdivisionBudget -> Search -> Map (ExactRational, Int) (ExactVector, Pair) -> Pair
-> Either (BudgetObligation, Search) Search
refine budget reached rest pair =
either (\obligation -> Left (obligation, reached)) Right $ do
children <- split budget pair
foldM (admit budget) (withFrontier rest reached) children
admit :: SubdivisionBudget -> Search -> Pair -> Either BudgetObligation Search
admit budget reached@(Search _ _ _ spent _ _) pair
| spent >= budgetLeaves budget = Left LeavesExhausted
| otherwise = enter budget reached pair
-- Evaluate a pair: its sites may improve the nearest, which prunes every
-- pair whose gap now exceeds it; the pair stays live if its gap does not and
-- one of its pieces can move; a watched crossing is recorded once. Every
-- value retained or compared is admitted first.
enter :: SubdivisionBudget -> Search -> Pair -> Either BudgetObligation Search
enter budget (Search frontier nearest crossing spent ordinal watching) pair = do
sites <- pairSites budget pair
(gapVector, gap) <- pairGap budget pair
let nearest' = foldl' closer nearest sites
bound = nearestSquared nearest'
kept
| gap <= bound && refinable pair = Map.insert (gap, ordinal) (gapVector, pair) frontier
| otherwise = frontier
crossing' <- case crossing of
Nothing | watching && gap == 0 -> pairCrossing budget pair
_ -> Right crossing
pure (Search (Map.takeWhileAntitone ((<= bound) . fst) kept) nearest' crossing' (spent + 1) (ordinal + 1) watching)
withFrontier :: Map (ExactRational, Int) (ExactVector, Pair) -> Search -> Search
withFrontier frontier (Search _ nearest crossing spent ordinal watching) =
Search frontier nearest crossing spent ordinal watching
split :: SubdivisionBudget -> Pair -> Either BudgetObligation [Pair]
split budget (Pair a b)
| refinable' a && (not (refinable' b) || extent a >= extent b) =
(\(left, right) -> [Pair left b, Pair right b]) <$> halve budget a
| refinable' b = (\(left, right) -> [Pair a left, Pair a right]) <$> halve budget b
| otherwise = Left DepthExhausted
where
refinable' piece'@(Piece depth _ _ _) = moving piece' && depth < budgetDepth budget
halve :: SubdivisionBudget -> Piece -> Either BudgetObligation (Piece, Piece)
halve budget (Piece depth source span' _) = do
let (left, right) = halveSpan span'
traverse_ (admitSpan budget) [left, right]
pure (piece (depth + 1) source left, piece (depth + 1) source right)
admitSource :: SubdivisionBudget -> Piece -> Either ProximityError Piece
admitSource budget source@(Piece _ subpath span' _) =
either (Left . ProximitySourceRefused (CurveSpan subpath span')) (const (Right source)) (admitSpan budget span')
sourcePieces :: Subpath -> [Piece]
sourcePieces source = [piece 0 source (wholeSpan step) | step <- toList (sourceSteps source)]
piece :: Int -> Subpath -> SourceSpan -> Piece
piece depth source span' = Piece depth source span' (exactPointsBounds (sourceSpanControls span'))
moving :: Piece -> Bool
moving (Piece _ _ span' _) = not (stationaryPiece (sourceSpanPiece span'))
refinable :: Pair -> Bool
refinable (Pair a b) = moving a || moving b
extent :: Piece -> ExactRational
extent (Piece _ _ _ box) = max (boundsMaximumX box - boundsMinimumX box) (boundsMaximumY box - boundsMinimumY box)
-- The shortest displacement between two pieces' control hulls, zero where
-- they meet, and its square, admitted. Each piece lies in its hull, so the
-- square bounds the pair's squared distance from below; unlike the control
-- boxes, the hulls of two concentric arcs close on the arcs quadratically.
pairGap :: SubdivisionBudget -> Pair -> Either BudgetObligation (ExactVector, ExactRational)
pairGap budget (Pair (Piece _ _ spanA _) (Piece _ _ spanB _)) =
(gap, squared) <$ admitWidth budget (max (vectorBits gap) (exactRationalBitWidth squared))
where
gap = hullGap (sourceSpanControls spanA) (sourceSpanControls spanB)
squared = dot gap gap
-- A certified crossing, its certificate admitted by 'crossingVerdict'.
pairCrossing :: SubdivisionBudget -> Pair -> Either BudgetObligation (Maybe (CurveSpan, CurveSpan, CrossingCertificate))
pairCrossing budget (Pair (Piece _ sourceA spanA _) (Piece _ sourceB spanB _)) =
fmap single (crossingVerdict budget (sourceSpanStart spanA) (sourceSpanPiece spanA) (sourceSpanStart spanB) (sourceSpanPiece spanB))
where
single verdict = case verdict of
Just (SingleCrossing certificate) -> Just (CurveSpan sourceA spanA, CurveSpan sourceB spanB, certificate)
_ -> Nothing
-- Every endpoint pairing, and each endpoint's nearest point on the other
-- piece when that piece is straight. An endpoint site is a located control
-- of an admitted span; the pairings are admitted or refuse, and a projected
-- pairing too wide is dropped.
pairSites :: SubdivisionBudget -> Pair -> Either BudgetObligation (NonEmpty Nearest)
pairSites budget (Pair a b) = do
corners <- traverse (admitNearest budget) (sitePair <$> ends a <*> ends b)
let projected =
mapMaybe (\siteB -> (`sitePair` siteB) <$> projection budget a (sitePoint siteB)) (toList (ends b))
<> mapMaybe (\siteA -> sitePair siteA <$> projection budget b (sitePoint siteA)) (toList (ends a))
pure (NonEmpty.appendList corners (filter (isRight . admitNearest budget) projected))
admitNearest :: SubdivisionBudget -> Nearest -> Either BudgetObligation Nearest
admitNearest budget near@(Nearest squared gap _ _) =
near <$ admitWidth budget (max (vectorBits gap) (exactRationalBitWidth squared))
-- A piece's two endpoint sites, at the points of its exact subdivision.
ends :: Piece -> NonEmpty TrailSite
ends (Piece _ source span' _) =
trailSite source step (sourceSpanFrom span') start
:| [trailSite source step (sourceSpanTo span') (translateExactPoint start (curveStepEnd (sourceSpanPiece span')))]
where
step = sourceSpanStep span'
start = sourceSpanStart span'
-- The nearest point of a straight piece to a point, as a site. A straight
-- piece is its step restricted to its bracket, affinely in the parameter, so
-- the piece's fraction @t@ is the step's parameter a fraction @t@ through the
-- bracket, and the point there is the step's. A site wider than the bit
-- budget is not retained.
projection :: SubdivisionBudget -> Piece -> ExactPoint -> Maybe TrailSite
projection budget (Piece _ source span' _) point = case shapeView (curveStepShape (sourceSpanPiece span')) of
LinearView -> do
squaredLength <- either (const Nothing) Just (positiveExact (dot direction direction))
let fraction = unitClamp (divideByPositive (dot (displacement start point) direction) squaredLength)
nearest = translateExactPoint start (scale (unitIntervalValue fraction) direction)
parameter = unitBetween (sourceSpanFrom span') (sourceSpanTo span') fraction
if max (exactPointBitWidth nearest) (exactRationalBitWidth (unitIntervalValue parameter)) <= budgetBits budget
then Just (trailSite source (sourceSpanStep span') parameter nearest)
else Nothing
_ -> Nothing
where
start = sourceSpanStart span'
direction = curveStepEnd (sourceSpanPiece span')
sitePair :: TrailSite -> TrailSite -> Nearest
sitePair siteA siteB = Nearest (dot gap gap) gap siteA siteB
where
gap = displacement (sitePoint siteA) (sitePoint siteB)
closer :: Nearest -> Nearest -> Nearest
closer current candidate
| nearestSquared candidate < nearestSquared current = candidate
| otherwise = current
nearestSquared :: Nearest -> ExactRational
nearestSquared (Nearest squared _ _ _) = squared
-- Root enclosures of the least live gap and of the nearest distance, both
-- ends of each admitted as Measure admits its enclosures.
enclosed :: SubdivisionBudget -> RadicalPrecision -> Search -> Either BudgetObligation (LengthEnclosure, LengthEnclosure)
enclosed budget precision (Search frontier (Nearest _ gap _ _) _ _ _ _) =
(lower, upper) <$ traverse_ (admitWidth budget . enclosureBits) [lower, upper]
where
upper = root gap
lower = maybe upper (root . fst . snd) (Map.lookupMin frontier)
root (ExactVector x y) = euclideanLengthEnclosure precision [(x, y)]
enclosureBits enclosure =
max (exactRationalBitWidth (lengthEnclosureLower enclosure)) (exactRationalBitWidth (lengthEnclosureUpper enclosure))
admitWidth :: SubdivisionBudget -> Int -> Either BudgetObligation ()
admitWidth budget width
| width > budgetBits budget = Left (BitsExhausted width)
| otherwise = Right ()
vectorBits :: ExactVector -> Int
vectorBits (ExactVector x y) = max (exactRationalBitWidth x) (exactRationalBitWidth y)
liveCandidates :: Search -> Seq ProximityCandidate
liveCandidates (Search frontier _ _ _ _ _) =
Seq.fromList
[ ProximityCandidate (CurveSpan sourceA spanA) (CurveSpan sourceB spanB)
| (_, Pair (Piece _ sourceA spanA _) (Piece _ sourceB spanB _)) <- Map.elems frontier ]
displacement :: ExactPoint -> ExactPoint -> ExactVector
displacement from to =
let (ax, ay) = exactPointCoordinates from
(bx, by) = exactPointCoordinates to
in ExactVector (bx - ax) (by - ay)
dot :: ExactVector -> ExactVector -> ExactRational
dot (ExactVector ax ay) (ExactVector bx by) = ax * bx + ay * by
scale :: ExactRational -> ExactVector -> ExactVector
scale factor (ExactVector x y) = ExactVector (factor * x) (factor * y)