File: AlgReals.hs

package info (click to toggle)
haskell-sbv 10.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 8,148 kB
  • sloc: haskell: 31,176; makefile: 4
file content (322 lines) | stat: -rw-r--r-- 16,648 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
-----------------------------------------------------------------------------
-- |
-- Module    : Data.SBV.Core.AlgReals
-- Copyright : (c) Levent Erkok
-- License   : BSD3
-- Maintainer: erkokl@gmail.com
-- Stability : experimental
--
-- Algebraic reals in Haskell.
-----------------------------------------------------------------------------

{-# LANGUAGE FlexibleInstances #-}

{-# OPTIONS_GHC -Wall -Werror -fno-warn-orphans #-}

module Data.SBV.Core.AlgReals (
             AlgReal(..)
           , AlgRealPoly(..)
           , RationalCV(..)
           , RealPoint(..), realPoint
           , mkPolyReal
           , algRealToSMTLib2
           , algRealToHaskell
           , algRealToRational
           , mergeAlgReals
           , isExactRational
           , algRealStructuralEqual
           , algRealStructuralCompare
           )
   where

import Data.Char       (isDigit)

import Data.List       (sortBy, isPrefixOf, partition)
import Data.Ratio      ((%), numerator, denominator)
import Data.Function   (on)
import System.Random
import Test.QuickCheck (Arbitrary(..))

import Numeric (readSigned, readFloat)

import Text.Read(readMaybe)

-- | Is the endpoint included in the interval?
data RealPoint a = OpenPoint   a -- ^ open: i.e., doesn't include the point
                 | ClosedPoint a -- ^ closed: i.e., includes the point
                 deriving (Show, Eq, Ord)

-- | Extract the point associated with the open-closed point
realPoint :: RealPoint a -> a
realPoint (OpenPoint   a) = a
realPoint (ClosedPoint a) = a

-- | Algebraic reals. Note that the representation is left abstract. We represent
-- rational results explicitly, while the roots-of-polynomials are represented
-- implicitly by their defining equation
data AlgReal = AlgRational Bool Rational                             -- ^ bool says it's exact (i.e., SMT-solver did not return it with ? at the end.)
             | AlgPolyRoot (Integer,  AlgRealPoly) (Maybe String)    -- ^ which root of this polynomial and an approximate decimal representation with given precision, if available
             | AlgInterval (RealPoint Rational) (RealPoint Rational) -- ^ interval, with low and high bounds

-- | Check whether a given argument is an exact rational
isExactRational :: AlgReal -> Bool
isExactRational (AlgRational True _) = True
isExactRational _                    = False

-- | A univariate polynomial, represented simply as a
-- coefficient list. For instance, "5x^3 + 2x - 5" is
-- represented as [(5, 3), (2, 1), (-5, 0)]
newtype AlgRealPoly = AlgRealPoly [(Integer, Integer)]
                   deriving (Eq, Ord)

-- | Construct a poly-root real with a given approximate value (either as a decimal, or polynomial-root)
mkPolyReal :: Either (Bool, String) (Integer, [(Integer, Integer)]) -> AlgReal
mkPolyReal (Left (exact, str))
 = case (str, break (== '.') str) of
      ("", _)                                -> AlgRational exact 0
      (_, (x, '.':y)) | all isDigit (x ++ y) -> AlgRational exact (read (x++y) % (10 ^ length y))
      (_, (x, ""))    | all isDigit x        -> AlgRational exact (read x % 1)
      _                                      ->
        -- CVC5 prints in division-rational form:
        case readMaybe (filter (/= ',') (map (\c -> if c == '/' then '%' else c) str)) :: Maybe Rational of
          Just r  -> AlgRational exact r
          Nothing -> error $ unlines [ "*** Data.SBV.mkPolyReal: Unable to read a number from:"
                                     , "***"
                                     , "*** " ++ str
                                     , "***"
                                     , "*** Please report this as a bug."
                                     ]
mkPolyReal (Right (k, coeffs))
 = AlgPolyRoot (k, AlgRealPoly (normalize coeffs)) Nothing
 where normalize :: [(Integer, Integer)] -> [(Integer, Integer)]
       normalize = merge . sortBy (flip compare `on` snd)
       merge []                     = []
       merge [x]                    = [x]
       merge ((a, b):r@((c, d):xs))
         | b == d                   = merge ((a+c, b):xs)
         | True                     = (a, b) : merge r

instance Show AlgRealPoly where
  show (AlgRealPoly xs) = chkEmpty (join (concat [term p | p@(_, x) <- xs, x /= 0])) ++ " = " ++ show c
     where c  = -1 * head ([k | (k, 0) <- xs] ++ [0])
           term ( 0, _) = []
           term ( 1, 1) = [ "x"]
           term ( 1, p) = [ "x^" ++ show p]
           term (-1, 1) = ["-x"]
           term (-1, p) = ["-x^" ++ show p]
           term (k,  1) = [show k ++ "x"]
           term (k,  p) = [show k ++ "x^" ++ show p]
           join []      = ""
           join (k:ks) = k ++ s ++ join ks
             where s = case ks of
                        []    -> ""
                        (y:_) | "-" `isPrefixOf` y -> ""
                              | "+" `isPrefixOf` y -> ""
                              | True               -> "+"
           chkEmpty s = if null s then "0" else s

instance Show AlgReal where
  show (AlgRational exact a)         = showRat exact a
  show (AlgPolyRoot (i, p) mbApprox) = "root(" ++ show i ++ ", " ++ show p ++ ")" ++ maybe "" app mbApprox
     where app v | last v == '?' = " = " ++ init v ++ "..."
                 | True          = " = " ++ v
  show (AlgInterval a b)         = case (a, b) of
                                     (OpenPoint   l, OpenPoint   h) -> "(" ++ show l ++ ", " ++ show h ++ ")"
                                     (OpenPoint   l, ClosedPoint h) -> "(" ++ show l ++ ", " ++ show h ++ "]"
                                     (ClosedPoint l, OpenPoint   h) -> "[" ++ show l ++ ", " ++ show h ++ ")"
                                     (ClosedPoint l, ClosedPoint h) -> "[" ++ show l ++ ", " ++ show h ++ "]"

-- lift unary op through an exact rational, otherwise bail
lift1 :: String -> (Rational -> Rational) -> AlgReal -> AlgReal
lift1 _  o (AlgRational e a) = AlgRational e (o a)
lift1 nm _ a                 = error $ "AlgReal." ++ nm ++ ": unsupported argument: " ++ show a

-- lift binary op through exact rationals, otherwise bail
lift2 :: String -> (Rational -> Rational -> Rational) -> AlgReal -> AlgReal -> AlgReal
lift2 _  o (AlgRational True a) (AlgRational True b) = AlgRational True (a `o` b)
lift2 nm _ a                    b                    = error $ "AlgReal." ++ nm ++ ": unsupported arguments: " ++ show (a, b)

-- The idea in the instances below is that we will fully support operations
-- on "AlgRational" AlgReals, but leave everything else undefined. When we are
-- on the Haskell side, the AlgReal's are *not* reachable. They only represent
-- return values from SMT solvers, which we should *not* need to manipulate.
instance Eq AlgReal where
  AlgRational True a == AlgRational True b = a == b
  a                  == b                  = error $ "AlgReal.==: unsupported arguments: " ++ show (a, b)

instance Ord AlgReal where
  AlgRational True a `compare` AlgRational True b = a `compare` b
  a                  `compare` b                  = error $ "AlgReal.compare: unsupported arguments: " ++ show (a, b)

-- | Structural equality for AlgReal; used when constants are Map keys
algRealStructuralEqual   :: AlgReal -> AlgReal -> Bool
AlgRational a b `algRealStructuralEqual` AlgRational c d = (a, b) == (c, d)
AlgPolyRoot a b `algRealStructuralEqual` AlgPolyRoot c d = (a, b) == (c, d)
_               `algRealStructuralEqual` _               = False

-- | Structural comparisons for AlgReal; used when constants are Map keys
algRealStructuralCompare :: AlgReal -> AlgReal -> Ordering
AlgRational a b `algRealStructuralCompare` AlgRational c d = (a, b) `compare` (c, d)
AlgRational _ _ `algRealStructuralCompare` AlgPolyRoot _ _ = LT
AlgRational _ _ `algRealStructuralCompare` AlgInterval _ _ = LT
AlgPolyRoot _ _ `algRealStructuralCompare` AlgRational _ _ = GT
AlgPolyRoot a b `algRealStructuralCompare` AlgPolyRoot c d = (a, b) `compare` (c, d)
AlgPolyRoot _ _ `algRealStructuralCompare` AlgInterval _ _ = LT
AlgInterval _ _ `algRealStructuralCompare` AlgRational _ _ = GT
AlgInterval _ _ `algRealStructuralCompare` AlgPolyRoot _ _ = GT
AlgInterval a b `algRealStructuralCompare` AlgInterval c d = (a, b) `compare` (c, d)

instance Num AlgReal where
  (+)         = lift2 "+"      (+)
  (*)         = lift2 "*"      (*)
  (-)         = lift2 "-"      (-)
  negate      = lift1 "negate" negate
  abs         = lift1 "abs"    abs
  signum      = lift1 "signum" signum
  fromInteger = AlgRational True . fromInteger

-- |  NB: Following the other types we have, we require `a/0` to be `0` for all a.
instance Fractional AlgReal where
  (AlgRational True _) / (AlgRational True b) | b == 0 = 0
  a                    / b                             = lift2 "/" (/) a b
  fromRational = AlgRational True

instance Real AlgReal where
  toRational (AlgRational True v) = v
  toRational x                    = error $ "AlgReal.toRational: Argument cannot be represented as a rational value: " ++ algRealToHaskell x

instance Random Rational where
  random g = (a % b', g'')
     where (a, g')  = random g
           (b, g'') = random g'
           b'       = if 0 < b then b else 1 - b -- ensures 0 < b

  randomR (l, h) g = (r * d + l, g'')
     where (b, g')  = random g
           b'       = if 0 < b then b else 1 - b -- ensures 0 < b
           (a, g'') = randomR (0, b') g'

           r = a % b'
           d = h - l

instance Random AlgReal where
  random g = let (a, g') = random g in (AlgRational True a, g')
  randomR (AlgRational True l, AlgRational True h) g = let (a, g') = randomR (l, h) g in (AlgRational True a, g')
  randomR lh                                       _ = error $ "AlgReal.randomR: unsupported bounds: " ++ show lh

-- | Render an 'AlgReal' as an SMTLib2 value. Only supports rationals for the time being.
algRealToSMTLib2 :: AlgReal -> String
algRealToSMTLib2 (AlgRational True r)
   | m == 0 = "0.0"
   | m < 0  = "(- (/ "  ++ show (abs m) ++ ".0 " ++ show n ++ ".0))"
   | True   =    "(/ "  ++ show m       ++ ".0 " ++ show n ++ ".0)"
  where (m, n) = (numerator r, denominator r)
algRealToSMTLib2 r@(AlgRational False _)
   = error $ "SBV: Unexpected inexact rational to be converted to SMTLib2: " ++ show r
algRealToSMTLib2 (AlgPolyRoot (i, AlgRealPoly xs) _) = "(root-obj (+ " ++ unwords (concatMap term xs) ++ ") " ++ show i ++ ")"
  where term (0, _) = []
        term (k, 0) = [coeff k]
        term (1, 1) = ["x"]
        term (1, p) = ["(^ x " ++ show p ++ ")"]
        term (k, 1) = ["(* " ++ coeff k ++ " x)"]
        term (k, p) = ["(* " ++ coeff k ++ " (^ x " ++ show p ++ "))"]
        coeff n | n < 0 = "(- " ++ show (abs n) ++ ")"
                | True  = show n
algRealToSMTLib2 r@AlgInterval{}
   = error $ "SBV: Unexpected inexact rational to be converted to SMTLib2: " ++ show r

-- | Render an 'AlgReal' as a Haskell value. Only supports rationals, since there is no corresponding
-- standard Haskell type that can represent root-of-polynomial variety.
algRealToHaskell :: AlgReal -> String
algRealToHaskell (AlgRational True r) = "((" ++ show r ++ ") :: Rational)"
algRealToHaskell r                    = error $ unlines [ ""
                                                        , "SBV.algRealToHaskell: Unsupported argument:"
                                                        , ""
                                                        , "   " ++ show r
                                                        , ""
                                                        , "represents an irrational number, and cannot be converted to a Haskell value."
                                                        ]

-- | Conversion from internal rationals to Haskell values
data RationalCV = RatIrreducible AlgReal                                   -- ^ Root of a polynomial, cannot be reduced
                | RatExact       Rational                                  -- ^ An exact rational
                | RatApprox      Rational                                  -- ^ An approximated value
                | RatInterval    (RealPoint Rational) (RealPoint Rational) -- ^ Interval. Can be open/closed on both ends.
                deriving Show

-- | Convert an 'AlgReal' to a 'Rational'. If the 'AlgReal' is exact, then you get a 'Left' value. Otherwise,
-- you get a 'Right' value which is simply an approximation.
algRealToRational :: AlgReal -> RationalCV
algRealToRational a = case a of
                        AlgRational True  r        -> RatExact r
                        AlgRational False r        -> RatExact r
                        AlgPolyRoot _     Nothing  -> RatIrreducible a
                        AlgPolyRoot _     (Just s) -> let trimmed = case reverse s of
                                                                     '.':'.':'.':rest -> reverse rest
                                                                     _                -> s
                                                      in case readSigned readFloat trimmed of
                                                           [(v, "")] -> RatApprox v
                                                           _         -> bad "represents a value that cannot be converted to a rational"
                        AlgInterval lo hi          -> RatInterval lo hi
   where bad w = error $ unlines [ ""
                                 , "SBV.algRealToRational: Unsupported argument:"
                                 , ""
                                 , "   " ++ show a
                                 , ""
                                 , w
                                 ]

-- Try to show a rational precisely if we can, with finite number of
-- digits. Otherwise, show it as a rational value.
showRat :: Bool -> Rational -> String
showRat exact r = p $ case f25 (denominator r) [] of
                       Nothing               -> show r   -- bail out, not precisely representable with finite digits
                       Just (noOfZeros, num) -> let present = length num
                                                in neg $ case noOfZeros `compare` present of
                                                           LT -> let (b, a) = splitAt (present - noOfZeros) num in b ++ "." ++ if null a then "0" else a
                                                           EQ -> "0." ++ num
                                                           GT -> "0." ++ replicate (noOfZeros - present) '0' ++ num
  where p   = if exact then id else (++ "...")
        neg = if r < 0 then ('-':) else id
        -- factor a number in 2's and 5's if possible
        -- If so, it'll return the number of digits after the zero
        -- to reach the next power of 10, and the numerator value scaled
        -- appropriately and shown as a string
        f25 :: Integer -> [Integer] -> Maybe (Int, String)
        f25 1 sofar = let (ts, fs)   = partition (== 2) sofar
                          lts        = length ts
                          lfs        = length fs
                          noOfZeros  = lts `max` lfs
                      in Just (noOfZeros, show (abs (numerator r)  * factor ts fs))
        f25 v sofar = let (q2, r2) = v `quotRem` 2
                          (q5, r5) = v `quotRem` 5
                      in case (r2, r5) of
                           (0, _) -> f25 q2 (2 : sofar)
                           (_, 0) -> f25 q5 (5 : sofar)
                           _      -> Nothing
        -- compute the next power of 10 we need to get to
        factor []     fs     = product [2 | _ <- fs]
        factor ts     []     = product [5 | _ <- ts]
        factor (_:ts) (_:fs) = factor ts fs

-- | Merge the representation of two algebraic reals, one assumed to be
-- in polynomial form, the other in decimal. Arguments can be the same
-- kind, so long as they are both rationals and equivalent; if not there
-- must be one that is precise. It's an error to pass anything
-- else to this function! (Used in reconstructing SMT counter-example values with reals).
mergeAlgReals :: String -> AlgReal -> AlgReal -> AlgReal
mergeAlgReals _ f@(AlgRational exact r) (AlgPolyRoot kp Nothing)
  | exact = f
  | True  = AlgPolyRoot kp (Just (showRat False r))
mergeAlgReals _ (AlgPolyRoot kp Nothing) f@(AlgRational exact r)
  | exact = f
  | True  = AlgPolyRoot kp (Just (showRat False r))
mergeAlgReals _ f@(AlgRational e1 r1) s@(AlgRational e2 r2)
  | (e1, r1) == (e2, r2) = f
  | e1                   = f
  | e2                   = s
mergeAlgReals m _ _ = error m

-- Quickcheck instance
instance Arbitrary AlgReal where
  arbitrary = AlgRational True `fmap` arbitrary