packages feed

moonlight-planar-1.1.0.0: src-core/Moonlight/Planar/Internal/ExactRational.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MagicHash #-}

-- | Normalized exact rational arithmetic without geometric dependencies.
module Moonlight.Planar.Internal.ExactRational
  ( ExactRational
  , ExactArithmeticError (..)
  , exactRational
  , exactRationalFromDouble
  , exactRationalFromFiniteDouble
  , exactRationalFromDyadic
  , exactRationalFromDyadicRatio
  , exactRationalFromNormalizedRatio
  , exactRationalNumerator
  , exactRationalDenominator
  , exactRationalBitWidth
  , exactRationalDenominatorBitWidth
  , exactRationalIsZero
  , exactDivide
  , exactSignum
  , PositiveExact
  , UnitInterval
  , ScalarRefinementError (..)
  , positiveExact
  , positiveExactValue
  , unitInterval
  , unitIntervalValue
  , unitZero
  , unitHalf
  , unitOne
  , positiveOne
  , positiveTwo
  , exactHalf
  , exactThird
  , blendPositive
  , divideByPositive
  , ratioPositive
  , multiplyPositive
  , positiveSumSquares
  ) where

import Control.DeepSeq (NFData (..))
import Data.Bits ((.&.), shiftL, shiftR)
import Data.Ratio (Ratio, (%))
import qualified Data.Ratio as Ratio
import GHC.Generics (Generic)
import GHC.Exts (Int (I#))
import GHC.Integer.Logarithms (integerLog2#)
import GHC.Real (Ratio ((:%)))

-- | A checked wrapper around a reduced ratio with a strictly positive
-- denominator. 'Ratio' owns normalization, including the unique zero
-- representation @0 / 1@.
newtype ExactRational = ExactRational (Ratio Integer)
  deriving stock (Eq, Ord, Show)
  deriving newtype (Num)

instance NFData ExactRational where
  rnf (ExactRational value) = rnf value
  {-# INLINE rnf #-}

-- | Typed refusals from exact rational construction and division.
data ExactArithmeticError
  = ExactZeroDenominator
  | ExactZeroDivisor
  | ExactNaNInput
  | ExactInfiniteInput
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass (NFData)

-- | Construct a reduced rational, moving any sign onto the numerator and
-- refusing a zero denominator.
exactRational :: Integer -> Integer -> Either ExactArithmeticError ExactRational
exactRational _ 0 = Left ExactZeroDenominator
exactRational numerator denominator = Right (ExactRational (numerator % denominator))

-- | Convert a binary64 value without loss, refusing NaN and infinities.
exactRationalFromDouble :: Double -> Either ExactArithmeticError ExactRational
exactRationalFromDouble value
  | isNaN value = Left ExactNaNInput
  | isInfinite value = Left ExactInfiniteInput
  | otherwise = Right (exactRationalFromFiniteDouble value)

-- | Convert an already-admitted finite coordinate without loss. The caller
-- supplies a finite value, such as a coordinate inside a validated
-- @QueryPoint@, so this worker does not repeat the admission refusal.
exactRationalFromFiniteDouble :: Double -> ExactRational
exactRationalFromFiniteDouble value =
  uncurry exactRationalFromDyadic (decodeFloat value)
{-# INLINE exactRationalFromFiniteDouble #-}

-- | Construct @numerator * 2^power@ without a partial denominator path.
exactRationalFromDyadic :: Integer -> Int -> ExactRational
exactRationalFromDyadic numerator power
  | numerator == 0 = ExactRational (0 :% 1)
  | power >= 0 = ExactRational ((numerator `shiftL` power) :% 1)
  | otherwise =
      let denominatorPower = negate power
          removableFactor =
            min denominatorPower (integerTrailingZeroBits numerator)
          reducedNumerator = numerator `shiftR` removableFactor
          reducedDenominator =
            1 `shiftL` (denominatorPower - removableFactor)
       in ExactRational (reducedNumerator :% reducedDenominator)
{-# INLINE exactRationalFromDyadic #-}

-- | The denominator of a dyadic rational is a power of two, so its complete
-- normalization requires only the numerator's least set bit. Calling generic
-- rational GCD here merely rediscovers that closed arithmetic fact.
integerTrailingZeroBits :: Integer -> Int
integerTrailingZeroBits value =
  let magnitude = abs value
   in I# (integerLog2# (magnitude .&. negate magnitude))
{-# INLINE integerTrailingZeroBits #-}

-- | Construct @(numerator / denominator) * 2^power@ with one final rational
-- normalization. This is the publication boundary for exact dyadic kernels:
-- their integer arithmetic must not pay a greatest-common-divisor reduction
-- after every intermediate operation.
exactRationalFromDyadicRatio
  :: Integer
  -> Integer
  -> Int
  -> Either ExactArithmeticError ExactRational
exactRationalFromDyadicRatio _ 0 _ = Left ExactZeroDivisor
exactRationalFromDyadicRatio 0 _ _ = Right (ExactRational (0 :% 1))
exactRationalFromDyadicRatio numerator denominator power =
  let numeratorFactor = integerTrailingZeroBits numerator
      denominatorFactor = integerTrailingZeroBits denominator
      oddNumerator = numerator `shiftR` numeratorFactor
      oddDenominator = denominator `shiftR` denominatorFactor
      residualPower = power + numeratorFactor - denominatorFactor
   in if residualPower >= 0
        then exactRational (oddNumerator `shiftL` residualPower) oddDenominator
        else exactRational oddNumerator (oddDenominator `shiftL` negate residualPower)
{-# INLINE exactRationalFromDyadicRatio #-}

-- | Internal bridge from the normalized carrier owned by @Data.Ratio@. This
-- exists for statically nonzero rational constants in exact kernels; public
-- callers continue through 'exactRational'.
exactRationalFromNormalizedRatio :: Ratio Integer -> ExactRational
exactRationalFromNormalizedRatio = ExactRational
{-# INLINE exactRationalFromNormalizedRatio #-}

-- | Read the reduced numerator.
exactRationalNumerator :: ExactRational -> Integer
exactRationalNumerator (ExactRational value) = Ratio.numerator value
{-# INLINE exactRationalNumerator #-}

-- | Read the strictly positive reduced denominator.
exactRationalDenominator :: ExactRational -> Integer
exactRationalDenominator (ExactRational value) = Ratio.denominator value
{-# INLINE exactRationalDenominator #-}

-- | Maximum bit width of the reduced numerator magnitude and strictly
-- positive denominator. This is an observation, not an arithmetic bound.
exactRationalBitWidth :: ExactRational -> Int
exactRationalBitWidth value =
  max
    (integerBitWidth (abs (exactRationalNumerator value)))
    (exactRationalDenominatorBitWidth value)
{-# INLINE exactRationalBitWidth #-}

-- | Bit width of the reduced, strictly positive denominator.
exactRationalDenominatorBitWidth :: ExactRational -> Int
exactRationalDenominatorBitWidth =
  integerBitWidth . exactRationalDenominator
{-# INLINE exactRationalDenominatorBitWidth #-}

integerBitWidth :: Integer -> Int
integerBitWidth value
  | value <= 0 = 0
  | otherwise = I# (integerLog2# value) + 1
{-# INLINE integerBitWidth #-}

-- | Test whether the exact value is zero.
exactRationalIsZero :: ExactRational -> Bool
exactRationalIsZero (ExactRational value) = Ratio.numerator value == 0
{-# INLINE exactRationalIsZero #-}

-- | Divide by a nonzero exact rational, refusing a zero divisor explicitly.
exactDivide
  :: ExactRational
  -> ExactRational
  -> Either ExactArithmeticError ExactRational
exactDivide (ExactRational left) (ExactRational right)
  | Ratio.numerator right == 0 = Left ExactZeroDivisor
  | otherwise = Right (ExactRational (left / right))

-- | Compare an exact rational with zero through its canonical numerator.
exactSignum :: ExactRational -> Ordering
exactSignum (ExactRational value) = compare (Ratio.numerator value) 0
{-# INLINE exactSignum #-}

-- | A strictly positive scalar. Only admission and positivity-preserving
-- arithmetic in this scalar owner can construct the witness.
newtype PositiveExact = PositiveExact ExactRational
  deriving stock (Eq, Ord, Show)
  deriving newtype (NFData)

-- | A rational parameter in the closed unit interval.
newtype UnitInterval = UnitInterval ExactRational
  deriving stock (Eq, Ord, Show)
  deriving newtype (NFData)

data ScalarRefinementError
  = ExactNotPositive !ExactRational
  | ExactOutsideUnitInterval !ExactRational
  deriving stock (Eq, Ord, Show)

positiveExact :: ExactRational -> Either ScalarRefinementError PositiveExact
positiveExact value
  | value > 0 = Right (PositiveExact value)
  | otherwise = Left (ExactNotPositive value)

positiveExactValue :: PositiveExact -> ExactRational
positiveExactValue (PositiveExact value) = value

unitInterval :: ExactRational -> Either ScalarRefinementError UnitInterval
unitInterval value
  | value >= 0 && value <= 1 = Right (UnitInterval value)
  | otherwise = Left (ExactOutsideUnitInterval value)

unitIntervalValue :: UnitInterval -> ExactRational
unitIntervalValue (UnitInterval value) = value

unitZero, unitHalf, unitOne :: UnitInterval
unitZero = UnitInterval 0
unitHalf = UnitInterval exactHalf
unitOne = UnitInterval 1

positiveOne, positiveTwo :: PositiveExact
positiveOne = PositiveExact 1
positiveTwo = PositiveExact 2

exactHalf, exactThird :: ExactRational
exactHalf = ExactRational (1 :% 2)
exactThird = ExactRational (1 :% 3)

-- | Closed-interval convex blending preserves strict positivity, including
-- either endpoint. No repeated admission is required by its consumers.
blendPositive :: UnitInterval -> PositiveExact -> PositiveExact -> PositiveExact
blendPositive (UnitInterval t) (PositiveExact a) (PositiveExact b) =
  PositiveExact ((1 - t) * a + t * b)

-- | The same Ratio division as 'exactDivide', with its nonzero precondition
-- already discharged by the opaque positive divisor.
divideByPositive :: ExactRational -> PositiveExact -> ExactRational
divideByPositive (ExactRational a) (PositiveExact (ExactRational b)) =
  ExactRational (a / b)

ratioPositive :: PositiveExact -> PositiveExact -> PositiveExact
ratioPositive (PositiveExact a) b = PositiveExact (divideByPositive a b)

-- | Products of admitted positive scalars remain positive without readmission.
multiplyPositive :: PositiveExact -> PositiveExact -> PositiveExact
multiplyPositive (PositiveExact a) (PositiveExact b) = PositiveExact (a * b)

-- | A squared Euclidean norm is zero exactly when both coordinates vanish.
-- The nonzero branch constructs its positivity witness without re-admission.
positiveSumSquares :: ExactRational -> ExactRational -> Maybe PositiveExact
positiveSumSquares x y
  | x == 0 && y == 0 = Nothing
  | otherwise = Just (PositiveExact (x * x + y * y))