Author: Fredrik Johansson
This notebook gives an introduction to Calcium (https://fredrikj.net/calcium/), using the builtin ctypes-based Python wrapper.
The source notebook file: https://github.com/fredrik-johansson/calcium/blob/master/doc/introduction.ipynb
To run this Notebook locally, you need to build Calcium and add calcium/pycalcium
to your Python path.
Caution: the ctypes wrapper is incomplete, slow, and has some problems (e.g. memory leaks). It is mainly intended for testing the library. A more robust Cython interface should eventually be developed for "production use", along with a Julia interface. Calcium itself is also still experimental.
# For convenience
from IPython.display import display
# Import the Calcium ctypes wrapper
# Don't "import *" at home -- this is for demonstration use only
from pyca import *
Quick example: Euler's identity $e^{\pi i} + 1 = 0$
exp(pi * i) + 1
The core types implemented in Calcium are the following:
pyca.fexpr
(fexpr_t
in C)pyca.qqbar
(qqbar_t
in C)pyca.ca
(ca_t
in C)The types have different roles, and it is important to understand their differences.
Symbolic expressions are the most flexible representation: they preserve the exact form of the input without performing any computation or simplification. For example, $\sqrt{2} / 2$, $1 / \sqrt{2}$ and $2^{1/2} / 2$ are all different as expressions (although they represent the same number). Symbolic expressions are not very useful on their own for computations, but they are convenient as input and output for other, more "computational" types.
Algebraic numbers represent elements of the field $\overline{\mathbb{Q}}$ in canonical form, consisting of the number's minimal polynomial together with an isolating complex interval for a unique root. Thus one number can only have one representation: $\sqrt{2} / 2$ and $1 / \sqrt{2}$ will evaluate to exactly the same qqbar
algebraic number. This is useful because the results are predictable and in a sense maximally simplified (contrary to the other types, it is impossible to construct a complicated qqbar
instance that represents 0 without trivially being 0). The downsides are that field operations are much more expensive than in the ca
representation, and in passing from a symbolic expression to a qqbar
, structural information (such as a simple closed form) may be lost.
Calcium numbers represent numbers as elements of fields $\mathbb{Q}(a_1,\ldots,a_n)$. Calcium constructs extension numbers $a_k$ and fields $\mathbb{Q}(a_1,\ldots,a_n)$ automatically and lazily, so from the point of view of the user Calcium numbers behave just like numbers. The extension numbers $a_k$ can be "absolute" algebraic numbers (qqbar
instances), but they can also be transcendental numbers like $\pi$ or $\exp(\sqrt{2} i)$. This representation is highly efficient for arithmetic, but in general does not guarantee a canonical form. Relations between extension elements (e.g. $\log(4) / \log(2) = 2$) are simplified automatically using ideal reduction, but Calcium will not be able to prove all relations, so it is possible to have a ca
instance that represents 0 without trivially being 0 (such instances are nevertheless handled in a mathematically rigorous way as discussed below). The ca
type can also represent special values and metavalues (signed and unsigned infinities, undefined, unknown).
In the pyca
namespace, lowercase names (example: sqrt
) denote ca
functions while while uppercase names (example: Sqrt
) denote fexpr
symbolic expressions. It is easy to construct symbolic expressions:
# Create a symbolic sum of three terms
expr = Add(Sqrt(2) / 2, 1 / Sqrt(2), 2 ** Div(1, 2) / 2)
# Display the formula
expr
type(expr)
Constant expressions can be passed as input to other types, resulting in evaluation:
ca(expr) # evaluate expr using ca arithmetic, producing a ca
type(_)
qqbar(expr) # evaluate expr using qqbar arithmetic, producing a qqbar
type(_)
Symbolic expressions are also generated automatically behind the scenes in order to display objects in LaTeX in the notebook (this is done in the examples above). You can of course also print objects in text form:
print(expr)
print(ca(expr))
print(qqbar(expr))
By default, qqbar
objects display as polynomial roots. To produce a closed-form expression (if possible), the qqbar.fexpr()
method can be called:
qqbar(expr).fexpr()
The fexpr
type provides methods for rudimentary manipulation (accessing subexpressions and so on).
expr = Add(Sqrt(2) / 2, 1 / Sqrt(2), 2 ** Div(1, 2) / 2)
print(expr.head())
print(expr.nargs())
print(expr.args())
print(expr.is_atom())
print(expr.args()[0].args()[1].is_atom())
print(expr.num_leaves())
print(expr.nwords())
print(expr.replace(2, Pi))
The .nstr()
method computes a numerical approximation using Arb, returning a decimal string:
print((Sqrt(2) / 2).nstr(30))
print((Exp(1 + 2j).nstr(10)))
# No symbolic simplification is done - Arb can generally not detect
# exact zeros, and zeros will be output in the form 0e-n
print((Exp(Pi*NumberI)).nstr(30))
print((Sqrt(2)/2 - 1/Sqrt(2)).nstr())
The .expanded_normal_form()
method puts the given formula in a canonical form as a formal rational expression.
x = fexpr("x"); y = fexpr("y")
A = (x+y)**5 * (x-y) * (x + 1)
B = (x**2 - y**2) * (x**2 - 1)
(A / B).expanded_normal_form()
Please note that .expanded_normal_form()
only simplifies rational arithmetic operations, treating anything non-arithmetical as an atomic node. For example, square roots are treated as atomic. It also does not simplify nodes recursively.
display((Sqrt(x) / Sqrt(x)).expanded_normal_form())
display((Sqrt(x) * Sqrt(x)).expanded_normal_form())
display((Sqrt(2*x) / Sqrt(x*2)).expanded_normal_form())
We will not do anything more sophisticated with symbolic expressions in this notebook; we will instead move on to describing exact numerical calculations using the ca
and qqbar
types.
Calcium numbers encompass rational numbers, of course:
ca(1) / 3
(ca(1) / 3) * 6
Irrational numbers result in extensions of $\mathbb{Q}$:
# Alternative syntax: ca(2).sqrt()
sqrt(2)
sqrt(2) ** 2
2 * pi
Field arithmetic produces numbers represented as formal fraction field elements:
pi * i * sqrt(2)
(pi * i * sqrt(2)) ** 2 # note the simplifications
((pi + i + sqrt(2)) / (pi + sqrt(2)))**3
Let us construct the golden ratio:
phi = (sqrt(5) + 1) / 2
phi
We compute the 200th Fibonacci number using Binet's formula:
(phi**200 - (1-phi)**200) / sqrt(5)
Depending on the operations, ca
arithmetic may result in different field representations of the same number:
display(sqrt(2)*sqrt(3))
display(sqrt(6))
The difference simplifies to zero:
display(sqrt(2)*sqrt(3) - sqrt(6))
display(sqrt(2)*sqrt(3) == sqrt(6))
Calcium will attempt to eliminate more complex extension numbers when it encounters extension numbers that are algebraically dependent. Here, it eliminates $\sqrt{6}$ from the expression, writing the result in terms of $\sqrt{2}$ and $\sqrt{3}$
sqrt(2)*sqrt(3) + sqrt(6)
(Implementation detail: the output shows that $\sqrt{6}$ is still kept as part of the field structure, to aid future simplifications, although it is unused in this particular field element.)
In many cases, it would be desirable to write the above result as an element of the simple algebraic number field $\mathbb{Q}(\sqrt{6})$ or $\mathbb{Q}(\sqrt{2} + \sqrt{3})$ instead. Calcium will always stay within a univariate field when performing field operations starting from a single extension number, but it will not automatically reduce multivariate number fields to univariate fields. In the future, Calcium will offer more customizability so that the user can choose between different behaviors concerning simplification of extension numbers and fields.
Predicates in Calcium have mathematically semantics: they return True or False only if Calcium can prove the result. When the truth value is unknown, Calcium says so explicitly; in pyca
, this is done by raising an exception (NotImplementedError
). Consider, as an example, testing whether $\exp(\varepsilon) = 1$ where $\varepsilon$ is a small number. With $\varepsilon = 10^{-1000}$, Calcium finds that the numbers are not equal:
eps = ca(10) ** (-1000)
exp(eps) == 1
With $\varepsilon = 10^{-10000}$, Calcium fails:
eps = ca(10) ** (-10000)
try:
exp(eps) == 1
except Exception as e:
print(e)
The comparison fails because the internal precision limit for numerical evaluation has been exceeded. The precision limit and many other settings are stored in a context object, which also serves as a cache of computed data (such as extension numbers and extension fields). The context object has type ca_ctx
. There is a default context called ctx_default
, with the following settings:
ctx_default
If we create a new context object with higher prec_limit
than the default value of 4096 bits, the computation succeeds:
ctx = ca_ctx(prec_limit=65536)
eps = ca(10, context=ctx) ** (-10000)
exp(eps) == 1
The intention is that the user will be able to create multiple "Calcium fields" for different purposes. Right now, context objects support only limited configurability.
Calcium implements a complete decision procedure for testing equality (or inequality) of algebraic numbers. This functionality is accessible using either qqbar
or ca
operations.
Rob Corless proposed checking the following identity from Bill Gosper. We will first construct the LHS and RHS as symbolic expressions. We could evaluate them directly with qqbar
or ca
operations, which would be more efficient, but this way we can print the input.
I = NumberI
lhs = Sqrt(36 + 3*(-54+35*I*Sqrt(3))**Div(1,3)*3**Div(1,3) + \
117/(-162+105*I*Sqrt(3))**Div(1,3))/3 + \
Sqrt(5)*(1296*I+840*Sqrt(3)-35*3**Div(5,6)*(-54+35*I*Sqrt(3))**Div(1,3)-\
54*I*(-162+105*I*Sqrt(3))**Div(1,3)+13*I*(-162+105*I*Sqrt(3))**Div(2,3))/(5*(162*I+105*Sqrt(3)))
rhs = Sqrt(5) + Sqrt(7)
display(Equal(lhs, rhs))
We can check numerically that the expressions agree:
print(lhs.nstr()); print(rhs.nstr())
Evaluating the expressions as qqbar
s gives the same result, proving the identity:
display(qqbar(lhs)); display(qqbar(rhs))
Explicitly:
qqbar(lhs) == qqbar(rhs)
We can also perform the verification using ca
arithmetic. The equality is not immediately apparent since the two expressions evaluate to elements of different extension fields of $\mathbb{Q}$:
display(ca(lhs))
display(ca(rhs))
Fortunately, the equality operator manages to prove that the values are really equal:
ca(lhs) == ca(rhs)
Indeed, in this case ca
arithmetic simplifies the difference of the expressions to 0 automatically:
ca(lhs) - ca(rhs)
If this example did not look challenging enough, you may try proving equality of the two huge expressions (involving 7000 operations) given in https://ask.sagemath.org/question/52653/equality-of-algebraic-numbers-given-by-huge-symbolic-expressions/. You will need a bit of string preprocessing to convert the given Sage expressions to fexpr
expressions or ordinary Python syntax for use with the ca
or qqbar
types.
This test problem is implemented in the huge_expr.c
program in the Calcium examples directory. It should take a few seconds to run.
Calcium is capable of manipulating numbers involving some transcendental functions such as exp
and log
. Contrary to the case of algebraic numbers, it does not have a complete decision procedure for transcendental numbers, but it has some decent heuristics.
Basic logarithmic and exponential simplifications work as expected:
log(10**20) / log(100)
exp(pi) * exp(-pi + log(2))
Different formulas for the same number may result in different internal field representations, in which case Calcium may yet be able to recognize relations when the numbers are subtracted or divided or when evaluating a predicate:
display(log(sqrt(2)))
display(log(2)/2)
display(log(sqrt(2)) - log(2)/2)
display(log(sqrt(2)) / (log(2)/2))
display(log(sqrt(2)) * (log(2)/2))
log(sqrt(2)) == log(2)/2
Calcium is aware of branch cuts:
# exp(log(x)) == x
display(exp(log(1 + 10*i)))
# log(exp(x)) != x in general; here we get the correct branch!
display(log(exp(1 + 10*i)))
Let us check the formulas given on the Calcium documentation front page:
log(sqrt(2)+sqrt(3)) / log(5 + 2*sqrt(6))
i**i - exp(pi / ((sqrt(-2)**sqrt(2)) ** sqrt(2)))
ca(10)**-30 < (640320**3 + 744)/exp(pi*sqrt(163)) - 1 < ca(10)**-29
Calcium does not yet have trigonometric or inverse trigonometric funtions builtin at the C level, but we can "fake" these functions using complex exponentials and logarithms. The functions pyca.sin
, pyca.cos
, pyca.tan
and pyca.atan
do precisely this.
sin(3)
Calcium is capable of proving some trigonometric identities:
sin(sqrt(2)/2)**2 + cos(1/sqrt(2))**2
sin(3 + pi) + sin(3)
tan(atan(5)) == 5
try:
atan(tan(1)) == 1
except NotImplementedError:
print("This simplification does not work yet")
We test some simplifications involving the Gudermannian function $\operatorname{gd}(x) = 2 \operatorname{atan}(e^x) - \pi/2$:
def gd(x):
return 2*atan(exp(x))-pi/2
display(sin(gd(1)) - tanh(1))
display(tan(gd(1)) - sinh(1))
display(sin(gd(sqrt(2))) - tanh(sqrt(2)))
display(tan(gd(1)/2) - tanh(ca(1)/2))
Let us try to prove a famous identity; Machin's formula for $\pi$:
lhs = 4*Atan(Div(1,5)) - Atan(Div(1,239))
rhs = Pi / 4
Equal(lhs, rhs)
print(lhs.nstr())
print(rhs.nstr())
Evaluating the left-hand side using ca
arithmetic gives us nothing as simple as $\pi / 4$:
4*atan(ca(1)/5) - atan(ca(1)/239)
Nevertheless, Calcium finds the identity when $\pi$ is given as part of the input:
4*atan(ca(1)/5) - atan(ca(1)/239) - pi/4
4*atan(ca(1)/5) - atan(ca(1)/239) == pi/4
Here is a more complicated formula:
12*atan(ca(1)/49) + 32*atan(ca(1)/57) - 5*atan(ca(1)/239) + 12*atan(ca(1)/110443)
12*atan(ca(1)/49) + 32*atan(ca(1)/57) - 5*atan(ca(1)/239) + 12*atan(ca(1)/110443) - pi/4
Hyperbolic formulas also work:
atanh = lambda x: -i*atan(i*x)
32*atanh(ca(1)/31) + 24*atanh(ca(1)/49) + 14*atanh(ca(1)/161)
32*atanh(ca(1)/31) + 24*atanh(ca(1)/49) + 14*atanh(ca(1)/161) - log(5)
The ca_mat
type provides matrices with ca
entries. We look at some examples of basic manipulation:
A = ca_mat([[1, i*pi], [-i*pi, 2]])
A
A * A
display(A.det())
display(A.trace())
display(A.rank())
Solving linear systems:
B = ca_mat([[1], [2]])
X = A.solve(B)
display(X)
display(A * X)
Computing row echelon forms and characteristic polynomials:
ca_mat([[1,pi,2,pi],[1/pi,3,1/(pi+1),4],[1,1,1,1]]).rref()
A = ca_mat([[5, pi], [1, -1]])**4
display(A)
display(A.charpoly())
display(A.charpoly()(A))
Calcium correctly recognizes singular matrices, even matrices that are nontrivially singular.
A = ca_mat([[pi, pi**2], [pi**3, pi**4]])
A
try:
A.solve(ca_mat([[1], [2]]))
except ZeroDivisionError:
print("matrix is singular!")
A.rref()
A.rank()
Exact matrix operations easily result in large expressions:
A = ca_mat([[sqrt(i+j+1) for i in range(6)] for j in range(6)])
A
A.det()
Calcium can calculate exact eigenvalues of matrices, with correct multiplicities. This is accomplished by factoring the characteristic polynomial. Currently, computing polynomial roots will only work for polynomials with very simple structure or with rational entries, so do not expect too much!
A = ca_mat([[1,pi],[-pi,1]])
display(A)
for lamda, mult in A.eigenvalues():
print("Multiplicity", mult)
display(lamda)
We demonstrate computing the Jordan decomposition of a matrix with nontrivial Jordan form:
A = ca_mat([[20,77,59,40], [0,-2,-3,-2], [-10,-35,-23,-15], [2,7,3,1]])
J, P = A.jordan_form(transform=True)
display(J)
display(P)
display(P * J * P.inv())
We construct a simple matrix and compute its matrix logarithm, which is uses Jordan decomposition internally:
A = ca_mat([[-1, -2], [1, 1]])
display(A)
display(A.log())
We evaluate the exponential of the logarithm, recovering the original matrix. This will only work in very simple cases.
A.log().exp()
Another nice example:
B = ca_mat([[0,0,1],[0,1,0],[1,0,0]])
display(B.log())
display(B.log().exp())
The following matrix is nilpotent; its exponential is a polynomial expression of the matrix:
A = ca_mat([[10,32,3,-13], [-8,-30,-4,11], [25,90,11,-34], [-6,-28,-5,9]])
display(A.exp())
display(A**0 + A**1 + A**2/2 + A**3/6)
We construct the 5x5 Hilbert matrix and check that its eigenvalues satisfy the expected determinant and trace relations:
H = ca_mat([[ca(1)/(i+j+1) for i in range(5)] for j in range(5)])
H
eig = H.eigenvalues()
for c, mult in eig:
display(c)
display(sum(c * mult for (c, mult) in eig)); display(H.trace())
display(prod(c ** mult for (c, mult) in eig)); display(H.det())
The ca_poly
type represents univariate polynomials with ca
coefficients. We can construct polynomials and do arithmetic:
x = ca_poly([0,1])
f = 1 + (2 + ca(2).sqrt() * ca.pi()) * x + 3*x**2
f
f ** 5
ca_poly([1,1,1,1,1,1,1,1,1]).integral()
Polynomial division, GCD and other operations work as expected:
display(f ** 5 // f**4)
display(f**5 % f**4)
(f * (x**2-1)).gcd(f**2 * (x-1))
(x**2 + ca.pi()**2).gcd(x + ca.i() * ca.pi())
ca_poly([9,6,7,-28,12]).squarefree_part()
# Squarefree factorization
for (fac, mult) in ca_poly([9,6,7,-28,12]).factor_squarefree()[1]:
print(f"Multiplicity {mult}:")
display(fac)
Finding the roots of a high-degree polynomial:
f = 4*x**7 + 4*x**6 - 11*x**5 - 16*x**4 + x**3 + 15*x**2 + 10*x + 2
for root, mult in f.roots():
print(f"Multiplicity {mult}:")
display(root)
f(sqrt(2))