from cas.store import (
C_HALF, C_ONE, C_PI, cas_add, cas_addv, cas_div, cas_fn, cas_int, cas_isint, cas_kid,
cas_kind, cas_mul, cas_name_of, cas_nk, cas_np, cas_nq, cas_num, cas_pow, cas_sqrt, cas_sub,
cas_sym, K_ADD, K_FN, K_MUL, K_NUM, K_POW, K_SYM,
)
from cas.rational import mp_add, mp_inv, mp_mul, mp_pow, mp_rat
# the double nearest pi
PI_DOUBLE = 3.141592653589793
# 2 / sqrt(pi), the bound on erf's slope
ERF_SLOPE = 1.1283791670955126
# two units of 2^-53: the rounding a ball adds for each operation
BALL_EPS = 2.220446049250313e-16
# the residues cas_mod gives when it has no residue
MOD_UNLUCKY = -1
MOD_NOT_RATIONAL = -2
# ---- doubles -----------------------------------------------------------------------
#| The value of `e` in doubles; `env` maps a symbol's id to its value.
def cas_eval(e, env) {
memo = map()
return cas_eval_rec(e, env, memo)
}
def cas_eval_rec(e, env, memo) {
if has(memo, e) {
return memo[e]
}
k = cas_kind(e)
v = 0.0 / 0.0
if k == K_NUM {
v = cas_np(e) / cas_nq(e)
} else if k == K_SYM {
if e == C_PI {
v = PI_DOUBLE
} else if has(env, e) {
v = env[e]
}
} else if k == K_ADD {
v = 0.0
for i to cas_nk(e) {
v = v + cas_eval_rec(cas_kid(e, i), env, memo)
}
} else if k == K_MUL {
v = 1.0
for i to cas_nk(e) {
v = v * cas_eval_rec(cas_kid(e, i), env, memo)
}
} else if k == K_POW {
b = cas_eval_rec(cas_kid(e, 0), env, memo)
x = cas_kid(e, 1)
xv = cas_eval_rec(x, env, memo)
if x == C_HALF {
v = sqrt(b)
} else if b < 0 and not cas_isint(x) {
v = 0.0 / 0.0
} else {
v = pow(b, xv)
}
} else if k == K_FN {
v = cas_apply(cas_name_of(e), cas_eval_rec(cas_kid(e, 0), env, memo))
}
memo[e] = v
return v
}
#| One of the CAS's function heads applied to a double.
def cas_apply(h, a) {
if h == "exp" {
return exp(a)
}
if h == "log" {
return log(a)
}
if h == "sin" {
return sin(a)
}
if h == "cos" {
return cos(a)
}
if h == "tan" {
return tan(a)
}
if h == "sinh" {
return sinh(a)
}
if h == "cosh" {
return cosh(a)
}
if h == "tanh" {
return tanh(a)
}
if h == "asin" {
return asin(a)
}
if h == "acos" {
return acos(a)
}
if h == "atan" {
return atan(a)
}
if h == "erf" {
return erf(a)
}
if h == "abs" {
return abs(a)
}
return 0.0 / 0.0
}
#| The value of `e` with no symbols but pi.
def cas_value(e) {
return cas_eval(e, map())
}
#| The value of `e` with the one symbol `x` at `v`.
def cas_value_at(e, x, v) {
env = map()
env[x] = v
return cas_eval(e, env)
}
# ---- mod p ---------------------------------------------------------------------------
#| The residue of `e` mod MOD_P; `env` maps a symbol's id to its residue.
def cas_mod(e, env) {
memo = map()
return cas_mod_rec(e, env, memo)
}
def cas_mod_rec(e, env, memo) {
if has(memo, e) {
return memo[e]
}
k = cas_kind(e)
v = MOD_NOT_RATIONAL
if k == K_NUM {
v = mp_rat(cas_np(e), cas_nq(e))
if v < 0 {
v = MOD_UNLUCKY
}
} else if k == K_SYM {
if e != C_PI and has(env, e) {
v = env[e]
}
} else if k == K_ADD or k == K_MUL {
v = 0
if k == K_MUL {
v = 1
}
i = 0
while i < cas_nk(e) and v >= 0 {
a = cas_mod_rec(cas_kid(e, i), env, memo)
if a < 0 {
v = a
} else if k == K_ADD {
v = mp_add(v, a)
} else {
v = mp_mul(v, a)
}
i = i + 1
}
} else if k == K_POW {
x = cas_kid(e, 1)
if cas_isint(x) {
b = cas_mod_rec(cas_kid(e, 0), env, memo)
if b < 0 {
v = b
} else if cas_np(x) >= 0 {
v = mp_pow(b, cas_np(x))
} else {
ib = mp_inv(b)
if ib < 0 {
v = MOD_UNLUCKY
} else {
v = mp_pow(ib, -cas_np(x))
}
}
}
}
memo[e] = v
return v
}
# ---- balls ---------------------------------------------------------------------------
#| A ball around `e`'s value, as midpoint and radius; `envm` and `envr` give
#| each symbol's midpoint and radius. A NaN radius means not known.
def cas_ball(e, envm, envr) {
mm = map()
mr = map()
return cas_ball_rec(e, envm, envr, mm, mr)
}
def ball_add(am, ar, bm, br) {
m = am + bm
return m, ar + br + BALL_EPS * abs(m)
}
def ball_mul(am, ar, bm, br) {
m = am * bm
return m, abs(am) * br + abs(bm) * ar + ar * br + BALL_EPS * abs(m)
}
def ball_div(am, ar, bm, br) {
if abs(bm) <= br {
return 0.0, 0.0 / 0.0
}
m = am / bm
return m, (ar + abs(m) * br) / (abs(bm) - br) + BALL_EPS * abs(m)
}
#| A ball around `h(a)` for a ball `(am, ar)`.
def ball_fn(h, am, ar) {
if ar != ar {
return 0.0, ar
}
m = cas_apply(h, am)
if m != m {
return 0.0, 0.0 / 0.0
}
own = 4.0 * BALL_EPS * (1.0 + abs(m))
if h == "exp" {
return m, abs(m) * (exp(ar) - 1.0) * (1.0 + BALL_EPS) + own
}
if h == "log" {
if am - ar <= 0 {
return 0.0, 0.0 / 0.0
}
return m, ar / (am - ar) + own
}
if h == "sin" or h == "cos" or h == "atan" or h == "tanh" or h == "abs" {
return m, ar + own
}
if h == "erf" {
return m, ERF_SLOPE * ar + own
}
if h == "sinh" or h == "cosh" {
return m, ar * cosh(abs(am) + ar) + own
}
if h == "tan" {
lo = am - ar
hi = am + ar
if ar >= 1 or cos(lo) * cos(hi) <= 0 {
return 0.0, 0.0 / 0.0
}
s = max(abs(tan(lo)), abs(tan(hi)))
return m, ar * (1.0 + s * s) * (1.0 + BALL_EPS) + own
}
if h == "asin" or h == "acos" {
t = abs(am) + ar
if t >= 1 {
return 0.0, 0.0 / 0.0
}
return m, ar / sqrt(1.0 - t * t) + own
}
return 0.0, 0.0 / 0.0
}
def cas_ball_rec(e, envm, envr, mm, mr) {
if has(mm, e) {
return mm[e], mr[e]
}
k = cas_kind(e)
m = 0.0
r = 0.0 / 0.0
if k == K_NUM {
m = cas_np(e) / cas_nq(e)
r = BALL_EPS * abs(m)
} else if k == K_SYM {
if e == C_PI {
m = PI_DOUBLE
r = BALL_EPS * PI_DOUBLE
} else if has(envm, e) {
m = envm[e]
r = envr[e]
}
} else if k == K_ADD {
m = 0.0
r = 0.0
for i to cas_nk(e) {
am, ar = cas_ball_rec(cas_kid(e, i), envm, envr, mm, mr)
m, r = ball_add(m, r, am, ar)
}
} else if k == K_MUL {
m = 1.0
r = 0.0
for i to cas_nk(e) {
am, ar = cas_ball_rec(cas_kid(e, i), envm, envr, mm, mr)
m, r = ball_mul(m, r, am, ar)
}
} else if k == K_POW {
bm, br = cas_ball_rec(cas_kid(e, 0), envm, envr, mm, mr)
x = cas_kid(e, 1)
if cas_isint(x) {
n = abs(cas_np(x))
m = 1.0
r = 0.0
for i to n {
m, r = ball_mul(m, r, bm, br)
}
if cas_np(x) < 0 {
m, r = ball_div(1.0, 0.0, m, r)
}
} else if x == C_HALF {
if bm - br < 0 {
r = 0.0 / 0.0
} else {
m = sqrt(bm)
r = br / (sqrt(bm - br) + m) + 4.0 * BALL_EPS * (1.0 + m)
}
} else {
# b^x = exp(x log b)
xm, xr = cas_ball_rec(x, envm, envr, mm, mr)
lm, lr = ball_fn("log", bm, br)
pm, pr = ball_mul(xm, xr, lm, lr)
m, r = ball_fn("exp", pm, pr)
}
} else if k == K_FN {
am, ar = cas_ball_rec(cas_kid(e, 0), envm, envr, mm, mr)
m, r = ball_fn(cas_name_of(e), am, ar)
}
mm[e] = m
mr[e] = r
return m, r
}
#| 1, -1 or 0: `e` is positive, negative, or not known to be either, for the
#| symbols in their balls.
def cas_sign(e, envm, envr) {
m, r = cas_ball(e, envm, envr)
if r != r {
return 0
}
if m - r > 0 {
return 1
}
if m + r < 0 {
return -1
}
return 0
}
test "doubles: exact numbers, pi and functions" {
x = cas_sym("x")
e = cas_addv([cas_pow(x, cas_int(2)), cas_fn("sin", x), cas_num(1, 3)])
assert_near(cas_value_at(e, x, 0.5), 0.25 + sin(0.5) + 1.0 / 3.0, 1.0e-16)
assert_near(cas_value(cas_mul(C_PI, C_HALF)), PI_DOUBLE / 2.0, 0)
assert_true(cas_value(cas_pow(cas_int(-8), cas_num(1, 3))) != cas_value(cas_pow(cas_int(-8), cas_num(1, 3))))
}
test "mod p: a rational function, an unlucky point, a function refused" {
x = cas_sym("x")
env = map()
env[x] = 3
e = cas_add(cas_pow(x, cas_int(2)), cas_div(C_ONE, x))
# 9 + 1/3
assert_near(cas_mod(e, env), mp_add(9, mp_rat(1, 3)), 0)
env[x] = 0
assert_near(cas_mod(e, env), MOD_UNLUCKY, 0)
assert_near(cas_mod(cas_fn("exp", x), env), MOD_NOT_RATIONAL, 0)
}
test "balls contain the value and decide signs" {
x = cas_sym("x")
em = map()
er = map()
em[x] = 1.5
er[x] = 0.5
# x in [1, 2]: x^2 - 5 is negative, log(x) is not negative, sqrt(x) - 2 is negative
assert_near(cas_sign(cas_sub(cas_pow(x, cas_int(2)), cas_int(5)), em, er), -1, 0)
assert_near(cas_sign(cas_sub(cas_sqrt(x), cas_int(2)), em, er), -1, 0)
assert_near(cas_sign(cas_fn("log", x), em, er), 0, 0)
m, r = cas_ball(cas_fn("exp", x), em, er)
assert_true(m - r <= exp(1.0) and m + r >= exp(2.0))
}
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: 2026 The Viv authors