NumPy

NumPy is not imported into sage initially. To use NumPy, you first need to import it.

sage: import numpy
sage: if int(numpy.version.short_version[0]) > 1:
....:     numpy.set_printoptions(legacy="1.25")  # to ensure numpy 2.0 compatibility
>>> from sage.all import *
>>> import numpy
>>> if int(numpy.version.short_version[Integer(0)]) > Integer(1):
...     numpy.set_printoptions(legacy="1.25")  # to ensure numpy 2.0 compatibility

The basic object of computation in NumPy is an array. It is simple to create an array.

sage: l = numpy.array([1,2,3])
sage: l
array([1, 2, 3])
>>> from sage.all import *
>>> l = numpy.array([Integer(1),Integer(2),Integer(3)])
>>> l
array([1, 2, 3])

NumPy arrays can store any type of python object. However, for speed, numeric types are automatically converted to native hardware types (i.e., int, float, etc.) when possible. If the value or precision of a number cannot be handled by a native hardware type, then an array of Sage objects will be created. You can do calculations on these arrays, but they may be slower than using native types. When the numpy array contains Sage or python objects, then the data type is explicitly printed as object. If no data type is explicitly shown when NumPy prints the array, the type is either a hardware float or int.

sage: l = numpy.array([2**40, 3**40, 4**40])
sage: l
array([1099511627776, 12157665459056928801, 1208925819614629174706176], dtype=object)
sage: a = 2.0000000000000000001
sage: a.prec() # higher precision than hardware floating point numbers
67
sage: numpy.array([a,2*a,3*a])
array([2.000000000000000000, 4.000000000000000000, 6.000000000000000000], dtype=object)
>>> from sage.all import *
>>> l = numpy.array([Integer(2)**Integer(40), Integer(3)**Integer(40), Integer(4)**Integer(40)])
>>> l
array([1099511627776, 12157665459056928801, 1208925819614629174706176], dtype=object)
>>> a = RealNumber('2.0000000000000000001')
>>> a.prec() # higher precision than hardware floating point numbers
67
>>> numpy.array([a,Integer(2)*a,Integer(3)*a])
array([2.000000000000000000, 4.000000000000000000, 6.000000000000000000], dtype=object)

The dtype attribute of an array tells you the type of the array. For fast numerical computations, you generally want this to be some sort of float. If the data type is float, then the array is stored as an array of machine floats, which takes up much less space and which can be operated on much faster.

sage: l = numpy.array([1.0, 2.0, 3.0])
sage: l.dtype
dtype('float64')
>>> from sage.all import *
>>> l = numpy.array([RealNumber('1.0'), RealNumber('2.0'), RealNumber('3.0')])
>>> l.dtype
dtype('float64')

You can create an array of a specific type by specifying the dtype parameter. If you want to make sure that you are dealing with machine floats, it is good to specify dtype=float when creating an array.

sage: l = numpy.array([1,2,3], dtype=float)
sage: l.dtype
dtype('float64')
>>> from sage.all import *
>>> l = numpy.array([Integer(1),Integer(2),Integer(3)], dtype=float)
>>> l.dtype
dtype('float64')

You can access elements of a NumPy array just like any list, as well as take slices

sage: l = numpy.array(range(10),dtype=float)
sage: l[3]
3.0
sage: l[3:6]
array([3., 4., 5.])
>>> from sage.all import *
>>> l = numpy.array(range(Integer(10)),dtype=float)
>>> l[Integer(3)]
3.0
>>> l[Integer(3):Integer(6)]
array([3., 4., 5.])

You can do basic arithmetic operations

sage: l+l
array([  0.,   2.,   4.,   6.,   8.,  10.,  12.,  14.,  16.,  18.])
sage: 2.5*l
array([  0. ,   2.5,   5. ,   7.5,  10. ,  12.5,  15. ,  17.5,  20. ,  22.5])
>>> from sage.all import *
>>> l+l
array([  0.,   2.,   4.,   6.,   8.,  10.,  12.,  14.,  16.,  18.])
>>> RealNumber('2.5')*l
array([  0. ,   2.5,   5. ,   7.5,  10. ,  12.5,  15. ,  17.5,  20. ,  22.5])

Note that l*l will multiply the elements of l componentwise. To get a dot product, use numpy.dot().

sage: l*l
array([  0.,   1.,   4.,   9.,  16.,  25.,  36.,  49.,  64.,  81.])
sage: numpy.dot(l,l)
285.0
>>> from sage.all import *
>>> l*l
array([  0.,   1.,   4.,   9.,  16.,  25.,  36.,  49.,  64.,  81.])
>>> numpy.dot(l,l)
285.0

We can also create two dimensional arrays

sage: m = numpy.array([[1,2],[3,4]])
sage: m
array([[1, 2],
       [3, 4]])
sage: m[1,1]
4
>>> from sage.all import *
>>> m = numpy.array([[Integer(1),Integer(2)],[Integer(3),Integer(4)]])
>>> m
array([[1, 2],
       [3, 4]])
>>> m[Integer(1),Integer(1)]
4

This is basically equivalent to the following

sage: m = numpy.matrix([[1,2],[3,4]])
sage: m
matrix([[1, 2],
        [3, 4]])
sage: m[0,1]
2
>>> from sage.all import *
>>> m = numpy.matrix([[Integer(1),Integer(2)],[Integer(3),Integer(4)]])
>>> m
matrix([[1, 2],
        [3, 4]])
>>> m[Integer(0),Integer(1)]
2

The difference is that with numpy.array(), m is treated as just an array of data. In particular m*m will multiply componentwise, however with numpy.matrix(), m*m will do matrix multiplication. We can also do matrix vector multiplication, and matrix addition

sage: n = numpy.matrix([[1,2],[3,4]],dtype=float)
sage: v = numpy.array([[1],[2]],dtype=float)
sage: n*v
matrix([[ 5.],
        [11.]])
sage: n+n
matrix([[2., 4.],
        [6., 8.]])
>>> from sage.all import *
>>> n = numpy.matrix([[Integer(1),Integer(2)],[Integer(3),Integer(4)]],dtype=float)
>>> v = numpy.array([[Integer(1)],[Integer(2)]],dtype=float)
>>> n*v
matrix([[ 5.],
        [11.]])
>>> n+n
matrix([[2., 4.],
        [6., 8.]])

If n was created with numpy.array(), then to do matrix vector multiplication, you would use numpy.dot(n,v).

All NumPy arrays have a shape attribute. This is a useful attribute to manipulate

sage: n = numpy.array(range(25),dtype=float)
sage: n
array([  0.,   1.,   2.,   3.,   4.,   5.,   6.,   7.,   8.,   9.,  10.,
        11.,  12.,  13.,  14.,  15.,  16.,  17.,  18.,  19.,  20.,  21.,
        22.,  23.,  24.])
sage: n.shape=(5,5)
sage: n
array([[ 0.,  1.,  2.,  3.,  4.],
       [ 5.,  6.,  7.,  8.,  9.],
       [10., 11., 12., 13., 14.],
       [15., 16., 17., 18., 19.],
       [20., 21., 22., 23., 24.]])
>>> from sage.all import *
>>> n = numpy.array(range(Integer(25)),dtype=float)
>>> n
array([  0.,   1.,   2.,   3.,   4.,   5.,   6.,   7.,   8.,   9.,  10.,
        11.,  12.,  13.,  14.,  15.,  16.,  17.,  18.,  19.,  20.,  21.,
        22.,  23.,  24.])
>>> n.shape=(Integer(5),Integer(5))
>>> n
array([[ 0.,  1.,  2.,  3.,  4.],
       [ 5.,  6.,  7.,  8.,  9.],
       [10., 11., 12., 13., 14.],
       [15., 16., 17., 18., 19.],
       [20., 21., 22., 23., 24.]])

This changes the one-dimensional array into a \(5\times 5\) array.

NumPy arrays can be sliced as well

sage: n = numpy.array(range(25),dtype=float)
sage: n.shape = (5,5)
sage: n[2:4,1:3]
array([[11., 12.],
       [16., 17.]])
>>> from sage.all import *
>>> n = numpy.array(range(Integer(25)),dtype=float)
>>> n.shape = (Integer(5),Integer(5))
>>> n[Integer(2):Integer(4),Integer(1):Integer(3)]
array([[11., 12.],
       [16., 17.]])

It is important to note that the sliced matrices are references to the original

sage: m = n[2:4,1:3]
sage: m[0,0] = 100
sage: n
array([[   0.,    1.,    2.,    3.,    4.],
       [   5.,    6.,    7.,    8.,    9.],
       [  10.,  100.,   12.,   13.,   14.],
       [  15.,   16.,   17.,   18.,   19.],
       [  20.,   21.,   22.,   23.,   24.]])
>>> from sage.all import *
>>> m = n[Integer(2):Integer(4),Integer(1):Integer(3)]
>>> m[Integer(0),Integer(0)] = Integer(100)
>>> n
array([[   0.,    1.,    2.,    3.,    4.],
       [   5.,    6.,    7.,    8.,    9.],
       [  10.,  100.,   12.,   13.,   14.],
       [  15.,   16.,   17.,   18.,   19.],
       [  20.,   21.,   22.,   23.,   24.]])

You will note that the original matrix changed. This may or may not be what you want. If you want to change the sliced matrix without changing the original you should make a copy

m=n[2:4,1:3].copy()

Some particularly useful commands are

sage: x = numpy.arange(0,2,.1,dtype=float)
sage: x
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. , 1.1, 1.2,
       1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9])
>>> from sage.all import *
>>> x = numpy.arange(Integer(0),Integer(2),RealNumber('.1'),dtype=float)
>>> x
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. , 1.1, 1.2,
       1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9])

You can see that numpy.arange() creates an array of floats increasing by 0.1 from 0 to 2. There is a useful command numpy.r_() that is best explained by example

sage: from numpy import r_
sage: j = complex(0,1)
sage: RealNumber = float
sage: Integer = int
sage: n = r_[0.0:5.0]
sage: n
array([0., 1., 2., 3., 4.])
sage: n = r_[0.0:5.0, [0.0]*5]
sage: n
array([0., 1., 2., 3., 4., 0., 0., 0., 0., 0.])
>>> from sage.all import *
>>> from numpy import r_
>>> j = complex(Integer(0),Integer(1))
>>> RealNumber = float
>>> Integer = int
>>> n = r_[RealNumber('0.0'):RealNumber('5.0')]
>>> n
array([0., 1., 2., 3., 4.])
>>> n = r_[RealNumber('0.0'):RealNumber('5.0'), [RealNumber('0.0')]*Integer(5)]
>>> n
array([0., 1., 2., 3., 4., 0., 0., 0., 0., 0.])

numpy.r_() provides a shorthand for constructing NumPy arrays efficiently. Note in the above 0.0:5.0 was shorthand for 0.0, 1.0, 2.0, 3.0, 4.0. Suppose we want to divide the interval from 0 to 5 into 10 intervals. We can do this as follows

sage: r_[0.0:5.0:11*j]
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. ])
>>> from sage.all import *
>>> r_[RealNumber('0.0'):RealNumber('5.0'):Integer(11)*j]
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. ])

The notation 0.0:5.0:11*j expands to a list of 11 equally space points between 0 and 5 including both endpoints. Note that j is the NumPy imaginary number, but it has this special syntax for creating arrays. We can combine all of these techniques

sage: n = r_[0.0:5.0:11*j,int(5)*[0.0],-5.0:0.0]
sage: n
array([ 0. ,  0.5,  1. ,  1.5,  2. ,  2.5,  3. ,  3.5,  4. ,  4.5,  5. ,
        0. ,  0. ,  0. ,  0. ,  0. , -5. , -4. , -3. , -2. , -1. ])
>>> from sage.all import *
>>> n = r_[RealNumber('0.0'):RealNumber('5.0'):Integer(11)*j,int(Integer(5))*[RealNumber('0.0')],-RealNumber('5.0'):RealNumber('0.0')]
>>> n
array([ 0. ,  0.5,  1. ,  1.5,  2. ,  2.5,  3. ,  3.5,  4. ,  4.5,  5. ,
        0. ,  0. ,  0. ,  0. ,  0. , -5. , -4. , -3. , -2. , -1. ])

Another useful command is numpy.meshgrid(), it produces meshed grids. As an example suppose you want to evaluate \(f(x,y)=x^2+y^2\) on a an equally spaced grid with \(\Delta x = \Delta y = .25\) for \(0\le x,y\le 1\). You can do that as follows

sage: import numpy
sage: j = complex(0,1)
sage: def f(x,y):
....:     return x**2+y**2
sage: from numpy import meshgrid
sage: x = numpy.r_[0.0:1.0:5*j]
sage: y = numpy.r_[0.0:1.0:5*j]
sage: xx,yy = meshgrid(x,y)
sage: xx
array([[0.  , 0.25, 0.5 , 0.75, 1.  ],
       [0.  , 0.25, 0.5 , 0.75, 1.  ],
       [0.  , 0.25, 0.5 , 0.75, 1.  ],
       [0.  , 0.25, 0.5 , 0.75, 1.  ],
       [0.  , 0.25, 0.5 , 0.75, 1.  ]])
sage: yy
array([[0.  , 0.  , 0.  , 0.  , 0.  ],
       [0.25, 0.25, 0.25, 0.25, 0.25],
       [0.5 , 0.5 , 0.5 , 0.5 , 0.5 ],
       [0.75, 0.75, 0.75, 0.75, 0.75],
       [1.  , 1.  , 1.  , 1.  , 1.  ]])
sage: f(xx,yy)
array([[0.    , 0.0625, 0.25  , 0.5625, 1.    ],
       [0.0625, 0.125 , 0.3125, 0.625 , 1.0625],
       [0.25  , 0.3125, 0.5   , 0.8125, 1.25  ],
       [0.5625, 0.625 , 0.8125, 1.125 , 1.5625],
       [1.    , 1.0625, 1.25  , 1.5625, 2.    ]])
>>> from sage.all import *
>>> import numpy
>>> j = complex(Integer(0),Integer(1))
>>> def f(x,y):
...     return x**Integer(2)+y**Integer(2)
>>> from numpy import meshgrid
>>> x = numpy.r_[RealNumber('0.0'):RealNumber('1.0'):Integer(5)*j]
>>> y = numpy.r_[RealNumber('0.0'):RealNumber('1.0'):Integer(5)*j]
>>> xx,yy = meshgrid(x,y)
>>> xx
array([[0.  , 0.25, 0.5 , 0.75, 1.  ],
       [0.  , 0.25, 0.5 , 0.75, 1.  ],
       [0.  , 0.25, 0.5 , 0.75, 1.  ],
       [0.  , 0.25, 0.5 , 0.75, 1.  ],
       [0.  , 0.25, 0.5 , 0.75, 1.  ]])
>>> yy
array([[0.  , 0.  , 0.  , 0.  , 0.  ],
       [0.25, 0.25, 0.25, 0.25, 0.25],
       [0.5 , 0.5 , 0.5 , 0.5 , 0.5 ],
       [0.75, 0.75, 0.75, 0.75, 0.75],
       [1.  , 1.  , 1.  , 1.  , 1.  ]])
>>> f(xx,yy)
array([[0.    , 0.0625, 0.25  , 0.5625, 1.    ],
       [0.0625, 0.125 , 0.3125, 0.625 , 1.0625],
       [0.25  , 0.3125, 0.5   , 0.8125, 1.25  ],
       [0.5625, 0.625 , 0.8125, 1.125 , 1.5625],
       [1.    , 1.0625, 1.25  , 1.5625, 2.    ]])

You can see that numpy.meshgrid() produces a pair of matrices, here denoted \(xx\) and \(yy\), such that \((xx[i,j],yy[i,j])\) has coordinates \((x[i],y[j])\). This is useful because to evaluate \(f\) over a grid, we only need to evaluate it on each pair of entries in \(xx\), \(yy\). Since NumPy automatically performs arithmetic operations on arrays componentwise, it is very easy to evaluate functions over a grid with very little code.

A useful module is the numpy.linalg module. If you want to solve an equation \(Ax=b\) do

sage: import numpy
sage: from numpy import linalg
sage: A = numpy.random.randn(5,5)
sage: b = numpy.array(range(1,6))
sage: x = linalg.solve(A,b)
sage: numpy.dot(A,x)
array([1., 2., 3., 4., 5.])
>>> from sage.all import *
>>> import numpy
>>> from numpy import linalg
>>> A = numpy.random.randn(Integer(5),Integer(5))
>>> b = numpy.array(range(Integer(1),Integer(6)))
>>> x = linalg.solve(A,b)
>>> numpy.dot(A,x)
array([1., 2., 3., 4., 5.])

This creates a random 5x5 matrix A, and solves \(Ax=b\) where b=[0.0,1.0,2.0,3.0,4.0]. There are many other routines in the numpy.linalg module that are mostly self-explanatory. For example there are qr and lu routines for doing QR and LU decompositions. There is also a command eigs for computing eigenvalues of a matrix. You can always do <function name>? to get the documentation which is quite good for these routines.

Hopefully this gives you a sense of what NumPy is like. You should explore the package as there is quite a bit more functionality.