packages feed

egison-5.1.0: hs-src/Language/Egison/Math/Rewrite.hs

{-# LANGUAGE QuasiQuotes #-}

{- |
Module      : Language.Egison.Math.Rewrite
Licence     : MIT

Residual mathematical rewrite rules kept in Haskell.

Remaining functions:
  - casRewriteDd : merge polynomial terms with same FunctionData factor.
    The equivalent declare-rule poly pattern is too expensive for complex
    differential-form samples (e.g. riemann-curvature-tensor-of-S2xS3).
  - casRewriteSqrt : sqrt power reduction and sqrt pair merging.
    Ported back from declare rule (G6 of design/cas-simplification.md):
    the Egison-level term rules paid a pattern-match attempt on every
    term of every sqrt-carrying value per normalization, making
    arithmetic on such values ~20x slower (thurston.egi's bottleneck).
  - casRewriteExp : exp power reduction and exp product merging
    ((exp a)^n -> exp (n a), exp a * exp b -> exp (a+b)), ported back
    for the same reason (an identical 60-operation fold on
    exp-carrying operands measured 20.7s under the declare rules).
    The value rules (exp 0 = 1, exp 1 = e, exp (n i pi) = (-1)^n)
    stay in the library.

Migrated to declare rule (and removed):
  - casRewriteI, casRewriteW, casRewriteLog
  - casRewritePower, casRewriteRt, casRewriteRtu
-}

module Language.Egison.Math.Rewrite
  ( casRewriteSymbol
  ) where

import           Control.Egison

import           Language.Egison.Math.CAS

-- | Apply rewrite rules to a CASValue.
casRewriteSymbol :: CASValue -> CASValue
casRewriteSymbol = casRewriteDd . casRewriteSqrt . casRewriteExp

-- | Rewrite sqrt factors of every term (top level of the value, the
-- same scope as casRewriteDd; inner values were normalized when they
-- were constructed):
--
--   1. Power reduction: (sqrt a)^n with |n| >= 2 becomes
--      a^q * (sqrt a)^r with q = quot n 2 and r = n - 2q in {-1,0,1}
--      (matching the power-evaluation path's normal form).
--   2. Pair merge: sqrt a * sqrt b (both with exponent 1) merges to
--      sqrt (a*b), and the square part of a single-term product is
--      extracted: sqrt 2 * sqrt 8 = sqrt 16 = 4,
--      sqrt (2x) * sqrt (2y) = 2 * sqrt (x y),
--      sqrt x * sqrt (x y^2) = x y.  Multi-term (polynomial) products
--      stay under the sqrt: sqrt (x+1) * sqrt (x-1) = sqrt (x^2-1).
--
-- Fast path: values none of whose terms need sqrt work (see
-- termNeedsSqrtWork) are returned unchanged, without any rebuilding.
casRewriteSqrt :: CASValue -> CASValue
casRewriteSqrt = go (100 :: Int)
 where
  -- Iterate to a fixpoint (like the old applyRuleFix): one rewrite
  -- pass can create new reducible shapes -- merging produces products
  -- whose casMult combines equal atoms into powers, and content
  -- splitting produces pairs that reproduce themselves identically
  -- (sqrt 2 * sqrt(-sqrt 5 - 5) merges and re-splits to the same
  -- pair, which the equality check turns into a fixpoint).
  go 0 v = v
  go fuel v =
    let v' = rewriteSqrtOnce v
    in if v' == v then v else go (fuel - 1) v'

rewriteSqrtOnce :: CASValue -> CASValue
rewriteSqrtOnce (CASFrac num denom)
  | valueNeedsSqrtWork num || valueNeedsSqrtWork denom =
      casDivide (rewriteSqrtPart num) (rewriteSqrtPart denom)
rewriteSqrtOnce v@(CASPoly _)
  | valueNeedsSqrtWork v = rewriteSqrtPart v
rewriteSqrtOnce v = v

-- | Fast path: a term needs sqrt work if it has a sqrt factor with
-- |exponent| >= 2, at least two sqrt factors with exponent 1, or a
-- nested value (an application argument, a quoted expression, or a
-- level-4 coefficient) that needs work itself -- the old declare-rule
-- versions recursed into those via mapTermAll, and nested-radical
-- reductions such as sqrt 5 * sqrt(-5-2 sqrt 5) * sqrt(-5+2 sqrt 5) = 5
-- depend on it.  Terms with a lone top-level (sqrt a)^(+-1) factor and
-- quiet insides -- the common shape in curvature-style values -- are
-- skipped without any rebuilding, which is the point of the port.
termNeedsSqrtWork :: CASTerm -> Bool
termNeedsSqrtWork (CASTerm c mono) = go 0 mono || valueNeedsSqrtWork c
 where
  go :: Int -> Monomial -> Bool
  go ones ((sym, n) : rest) = case sqrtRadicand sym of
    Just _
      | abs n >= 2       -> True
      | n == 1 && ones >= 1 -> True
      | n == 1           -> symNeedsSqrtWork sym || go 1 rest
      | otherwise        -> symNeedsSqrtWork sym || go ones rest
    Nothing              -> symNeedsSqrtWork sym || go ones rest
  go _ [] = False

symNeedsSqrtWork :: SymbolExpr -> Bool
symNeedsSqrtWork = symNeedsWorkWith valueNeedsSqrtWork

valueNeedsSqrtWork :: CASValue -> Bool
valueNeedsSqrtWork (CASFrac n d) = valueNeedsSqrtWork n || valueNeedsSqrtWork d
valueNeedsSqrtWork (CASPoly ts)  = any termNeedsSqrtWork ts
valueNeedsSqrtWork _             = False

-- | Rewrite the terms that need it and re-combine.  Combination goes
-- through casPlus/casDivide (not a raw CASPoly rebuild) because a
-- negative radicand power over a polynomial radicand produces a
-- genuine fraction: (sqrt (x+1))^-2 = 1/(x+1).
rewriteSqrtPart :: CASValue -> CASValue
rewriteSqrtPart (CASPoly ts) =
  let changed   = filter termNeedsSqrtWork ts
      unchanged = filter (not . termNeedsSqrtWork) ts
  in foldl casPlus (casNormalizePoly unchanged) (map rewriteSqrtTerm changed)
rewriteSqrtPart v = v

-- | The argument of a unary application factor of the named function.
applyArg1 :: String -> SymbolExpr -> Maybe CASValue
applyArg1 name (Apply1 fh a) = case fh of
  CASFactor (QuoteFunction w)
    | prettyFunctionName w == Just name -> Just a
  CASPoly [CASTerm (CASInteger 1) [(QuoteFunction w, 1)]]
    | prettyFunctionName w == Just name -> Just a
  _ -> Nothing
applyArg1 _ _ = Nothing

-- | The radicand of a sqrt application factor, if it is one.
sqrtRadicand :: SymbolExpr -> Maybe CASValue
sqrtRadicand = applyArg1 "sqrt"

-- | Generic traversals shared by the factor rewriters: does any
-- nested value (application argument, quoted expression) satisfy the
-- check / rewrite every nested value.  Each rewriter recurses only
-- into itself, matching the old per-rule mapTermAll recursion.
symNeedsWorkWith :: (CASValue -> Bool) -> SymbolExpr -> Bool
symNeedsWorkWith p (Apply1 _ a)       = p a
symNeedsWorkWith p (Apply2 _ a b)     = p a || p b
symNeedsWorkWith p (Apply3 _ a b c)   = p a || p b || p c
symNeedsWorkWith p (Apply4 _ a b c d) = p a || p b || p c || p d
symNeedsWorkWith p (Quote q)          = p q
symNeedsWorkWith _ _                  = False

rewriteInsideSymWith :: (CASValue -> CASValue) -> SymbolExpr -> SymbolExpr
rewriteInsideSymWith f sym = case sym of
  Apply1 g a       -> Apply1 g (f a)
  Apply2 g a b     -> Apply2 g (f a) (f b)
  Apply3 g a b c   -> Apply3 g (f a) (f b) (f c)
  Apply4 g a b c d -> Apply4 g (f a) (f b) (f c) (f d)
  Quote q          -> Quote (f q)
  _                -> sym

-- | Rewrite the sqrt factors of one term.  Returns a CASValue because
-- extracted radicand powers can be polynomials.  Nested values
-- (application arguments, quoted expressions, level-4 coefficients)
-- are rewritten first, bottom-up, matching the old mapTermAll
-- recursion of the declare-rule versions.
rewriteSqrtTerm :: CASTerm -> CASValue
rewriteSqrtTerm (CASTerm c0 mono0) =
  let c    = casRewriteSqrt c0
      mono = [ (rewriteInsideSym sym, n) | (sym, n) <- mono0 ]
      -- 1. power reduction on every factor with |n| >= 2
      (powerOuts, mono1) = foldr powerStep ([], []) mono
      powerStep (sym, n) (outs, ms) = case sqrtRadicand sym of
        Just a | abs n >= 2 ->
          let q = n `quot` 2
              r = n - 2 * q
          in (casPower a q : outs, if r == 0 then ms else (sym, r) : ms)
        _ -> (outs, (sym, n) : ms)
      -- 2. pair merge on the remaining exponent-1 sqrt factors
      (sqrtOnes, others) = partitionSqrtOnes mono1
      base = CASPoly [CASTerm c others]
      merged = case sqrtOnes of
        ((sym0, _) : _ : _) ->
          let radicands = map snd sqrtOnes
              product'  = casRewriteSqrt (foldr1 casMult radicands)
              (outside, insides) = splitSquarePart product'
              sqrtAtom r = case sym0 of
                Apply1 fh _ -> CASPoly [CASTerm (CASInteger 1) [(Apply1 fh r, 1)]]
                _           -> error "rewriteSqrtTerm: non-Apply1 sqrt factor"
          in outside : map sqrtAtom insides
        [(sym0, _)] -> [CASPoly [CASTerm (CASInteger 1) [(sym0, 1)]]]
        []          -> []
  in foldl casMult base (powerOuts ++ merged)
 where
  partitionSqrtOnes = foldr step ([], [])
   where
    step (sym, 1) (sq, rest) = case sqrtRadicand sym of
      Just a  -> ((sym, a) : sq, rest)
      Nothing -> (sq, (sym, 1) : rest)
    step f (sq, rest) = (sq, f : rest)

  rewriteInsideSym = rewriteInsideSymWith casRewriteSqrt

-- | Split a merged radicand into (outside, inside radicands) with
-- value = outside^2 * product(insides), extracting square content and
-- canonicalizing the atom forms:
--
--   * single-term radicand: extract the square part of the integer
--     coefficient and the even exponents (sqrt 16 = 4,
--     sqrt (4 x^2 y) = 2 x sqrt y); the remainder stays as ONE atom.
--   * multi-term radicand: extract the integer content's square part,
--     and split the squarefree content off as its OWN integer sqrt
--     atom, leaving a content-free polynomial radicand.  This matches
--     the stable form the old declare-rule + lib-sqrt round trip
--     converged to (sqrt 5 * sqrt(-10 sqrt 5 - 50) =
--     5 * sqrt(-2 sqrt 5 - 10)); without it, algebraically related
--     atoms appear in several content forms and sums that should
--     cancel (5th roots of unity, mini-test 117) do not.
--
-- Anything unsupported stays entirely inside (value-safe).
splitSquarePart :: CASValue -> (CASValue, [CASValue])
splitSquarePart (CASPoly [CASTerm (CASInteger m) mono])
  | m >= 0 =
      let (s, m') = integerSquarePart m
          outsideFactors = [ (sym, k `div` 2) | (sym, k) <- mono, k `div` 2 /= 0 ]
          insideFactors  = [ (sym, k `mod` 2) | (sym, k) <- mono, k `mod` 2 /= 0 ]
          outside = CASPoly [CASTerm (CASInteger s) outsideFactors]
          inside  = casNormalizePoly [CASTerm (CASInteger m') insideFactors]
      in (outside, if inside == CASInteger 1 then [] else [inside])
splitSquarePart (CASInteger m)
  | m >= 0 =
      let (s, m') = integerSquarePart m
      in (CASInteger s, if m' == 1 then [] else [CASInteger m'])
splitSquarePart (CASPoly ts@(_ : _ : _))
  | Just coeffs <- mapM intCoeff ts
  , let g = foldr1 gcd (map abs coeffs)
  , g > 1 =
      let (s, g') = integerSquarePart g
          prim = casNormalizePoly
                   [ CASTerm (CASInteger (c `div` g)) mono | CASTerm (CASInteger c) mono <- ts ]
      in ( CASInteger s
         , (if g' == 1 then [] else [CASInteger g']) ++ [prim] )
 where
  intCoeff (CASTerm (CASInteger c) _) = Just c
  intCoeff _                          = Nothing
splitSquarePart v = (CASInteger 1, [v])

-- | Largest s with s^2 dividing m (m >= 0), by trial division with a
-- divisor cap: square factors hiding behind primes above the cap are
-- left inside (value-safe, just less extraction).
integerSquarePart :: Integer -> (Integer, Integer)
integerSquarePart m0 = go m0 2
 where
  go :: Integer -> Integer -> (Integer, Integer)
  cap = 1000000
  go m p
    | p > cap || p * p > m = (1, m)
    | m `mod` p == 0 =
        let (e, m')   = strip m p 0
            (s, rest) = go m' (p + 1)
        in (p ^ (e `div` 2) * s, p ^ (e `mod` 2) * rest)
    | otherwise = go m (p + 1)
  strip :: Integer -> Integer -> Integer -> (Integer, Integer)
  strip m p e
    | m `mod` p == 0 = strip (m `div` p) p (e + 1)
    | otherwise      = (e, m)

-- | Rewrite exp factors of every term:
--
--   1. Power reduction: (exp a)^n with n /= 1 becomes exp (n a).
--   2. Product merge: all exp factors of a term merge into one,
--      exp a * exp b = exp (a+b); a zero total collapses to 1
--      (exp x * exp (-x) = 1).
--
-- One pass per term suffices (the merge is closed: it produces at
-- most one exp factor with exponent 1).  Nested values are rewritten
-- first, like casRewriteSqrt.  The value rules exp 0 = 1, exp 1 = e,
-- and exp (n i pi) = (-1)^n stay in the library and fire on the
-- merged atom as before.
casRewriteExp :: CASValue -> CASValue
casRewriteExp (CASFrac num denom)
  | valueNeedsExpWork num || valueNeedsExpWork denom =
      casDivide (rewriteExpPart num) (rewriteExpPart denom)
casRewriteExp v@(CASPoly _)
  | valueNeedsExpWork v = rewriteExpPart v
casRewriteExp v = v

expArg :: SymbolExpr -> Maybe CASValue
expArg = applyArg1 "exp"

-- | A term needs exp work if it has an exp factor with exponent /= 1,
-- at least two exp factors, or nested work.
termNeedsExpWork :: CASTerm -> Bool
termNeedsExpWork (CASTerm c mono) = go 0 mono || valueNeedsExpWork c
 where
  go :: Int -> Monomial -> Bool
  go ones ((sym, n) : rest) = case expArg sym of
    Just _
      | n /= 1    -> True
      | ones >= 1 -> True
      | otherwise -> symNeedsWorkWith valueNeedsExpWork sym || go 1 rest
    Nothing       -> symNeedsWorkWith valueNeedsExpWork sym || go ones rest
  go _ [] = False

valueNeedsExpWork :: CASValue -> Bool
valueNeedsExpWork (CASFrac n d) = valueNeedsExpWork n || valueNeedsExpWork d
valueNeedsExpWork (CASPoly ts)  = any termNeedsExpWork ts
valueNeedsExpWork _             = False

rewriteExpPart :: CASValue -> CASValue
rewriteExpPart (CASPoly ts) =
  let changed   = filter termNeedsExpWork ts
      unchanged = filter (not . termNeedsExpWork) ts
  in foldl casPlus (casNormalizePoly unchanged) (map rewriteExpTerm changed)
rewriteExpPart v = v

rewriteExpTerm :: CASTerm -> CASValue
rewriteExpTerm (CASTerm c0 mono0) =
  let c    = casRewriteExp c0
      mono = [ (rewriteInsideSymWith casRewriteExp sym, n) | (sym, n) <- mono0 ]
      (exps, others) = foldr step ([], []) mono
      step (sym, n) (es, ms) = case expArg sym of
        Just a  -> ((sym, casMult (CASInteger n) a) : es, ms)
        Nothing -> (es, (sym, n) : ms)
      base = CASPoly [CASTerm c others]
  in case exps of
       [] -> base
       ((sym0, _) : _) ->
         let total = foldr1 casPlus (map snd exps)
             atom = case sym0 of
               Apply1 fh _ -> CASPoly [CASTerm (CASInteger 1) [(Apply1 fh total, 1)]]
               _           -> error "rewriteExpTerm: non-Apply1 exp factor"
         in if casIsZero total
              then base
              else casMult base atom

-- | Rewrite dd (differential): merge polynomial terms whose monomial shares
-- the same FunctionData factor (same `g`, `args`, exponent, and rest of
-- monomial). Kept in Haskell because the equivalent
-- `declare rule auto poly` (multi-term, same-binding) is too expensive for
-- complex differential-form samples (e.g. riemann-curvature-tensor-of-S2xS3).
--
-- Fast path: if the value contains no FunctionData factor anywhere, the
-- pattern can't fire — return the value unchanged. This avoids the
-- per-mathNormalize-call overhead of `rewriteDdPoly`'s multi-term matcher
-- on the vast majority of values that have no `Function _ _` factors.
casRewriteDd :: CASValue -> CASValue
casRewriteDd v
  | not (casHasFunctionData v) = v
casRewriteDd (CASFrac num denom) =
  CASFrac (casNormalizePoly (rewriteDdPoly (extractTerms num)))
         (casNormalizePoly (rewriteDdPoly (extractTerms denom)))
 where
  extractTerms (CASPoly ts) = ts
  extractTerms (CASInteger n) = [CASTerm (CASInteger n) []]
  extractTerms _ = []
casRewriteDd (CASPoly ts) = casNormalizePoly (rewriteDdPoly ts)
casRewriteDd v = v

-- | True if the CASValue contains any `FunctionData` factor anywhere in
-- its tree.
casHasFunctionData :: CASValue -> Bool
casHasFunctionData = goV
 where
  goV (CASInteger _)  = False
  goV (CASFactor sym) = goSym sym
  goV (CASPoly terms) = any goTerm terms
  goV (CASFrac n d)   = goV n || goV d
  goTerm (CASTerm coeff mono) = goV coeff || any (goSym . fst) mono
  goSym (FunctionData {})  = True
  goSym (Apply1 f a)        = goV f || goV a
  goSym (Apply2 f a b)      = goV f || goV a || goV b
  goSym (Apply3 f a b c)    = goV f || goV a || goV b || goV c
  goSym (Apply4 f a b c d)  = goV f || goV a || goV b || goV c || goV d
  goSym (Quote v)           = goV v
  goSym (Symbol _ _ _)      = False
  goSym (QuoteFunction _)   = False

rewriteDdPoly :: [CASTerm] -> [CASTerm]
rewriteDdPoly poly =
  match dfs poly (Multiset CASTermM)
    [ [mc| casTerm' $a (($f & casFunc $g $args, $n) : $mr) :
           casTerm' $b ((casFunc #g #args, #n) : #mr) : $pr ->
             rewriteDdPoly (CASTerm (casPlus a b) ((f, n) : mr) : pr) |]
    , [mc| _ -> poly |]
    ]