SageMath-logo.png

Introducción a SageMath

¿Qué es SageMath?

SageMath es un sistema de álgebra computacional nacido en 2005 de la mano del matemático estadounidense William A. Stein. Inicialmente se llamó SAGE, acrónimo de System for Algebra and Geometry Experimentation, denominación cambiada posteriormente para evitar confusiones con otros programas.

SageMath fue concebido con el propósito de construir una alternativa de software libre a programas comerciales como Mathematica, Matlab, Maple o Magma. Todos estos programas proporcionan, en mayor o menor medida, un entorno en el que desarrollar, entre otras, tareas de cálculo simbólico, cálculo numérico, análisis estadístico, programación o representación gráfica de funciones y datos. Son, por ello, programas muy usados y apreciados en los ámbitos científicos y de ingeniería. Presentan, sin embargo, varios inconvenientes. Por un lado, las licencias de uso suelen tener un precio elevado, poco asequible fuera de los ámbitos empresariales o académicos. Por otra parte, cada programa tiene su propio lenguaje, que es preciso aprender. Pero, sobre todo, se trata de programas de código cerrado: es imposible acceder a la implementación de las funciones y rutinas que ofrecen, lo cual dificulta notablemente la verificación de resultados o la corrección de los errores que todo programa inevitablemente conlleva.

SageMath sigue un modelo distinto, bien representado por esta ilustración, obra de Martin Albrecht:

SageMath-car.jpg

En lugar de desarrollar por entero cada parte del sistema, SageMath amalgama más de 100 paquetes de software matemático (Maxima, GAP, Pari/GP, Numpy, Scipy...), todos de código abierto, a los que se añaden multitud de módulos escritos en Python para proporcionar una interfaz común a todos los programas e incrementar la funcionalidad de todo el sistema. Como expresa uno de sus lemas, SageMath «no reinventa la rueda», sino que aprovecha e integra software libre ya existente. Se construye así un potente «automóvil» computacional con múltiples beneficios para el usuario. En primer lugar, no es necesario aprender el lenguaje específico de cada programa, sino que basta con usar Python para comunicarse con todos ellos y trasvasar resultados de uno a otro, todo ello, además, de forma transparente. SageMath es gratuito, se puede descargar y usar libremente. Por último, SageMath es un sistema de código abierto: cualquiera puede examinar el código, extenderlo, proponer mejoras o corregir errores.

Acceso a SageMath

Tres son las vías principales de acceso a SageMath:

  • SageMathCell: Es una página web facilitada por una red de servidores distribuidos por todo el mundo. Presenta un cuadro en el que introducir y ejecutar código escrito en SageMath (y también en Python y otros lenguajes). Cuenta con una API para integrar SageMathCell en cualquier página web.

  • CoCalc: Es un servicio en línea que facilita un entorno completo de computación en el que se incluye SageMath. Ofrece cuentas gratuitas, con capacidades suficientes para empezar a experimentar, y cuentas de pago para un uso más serio o profesional.

  • Instalación local: SageMath es un software totalmente gratuito. Se obtiene mediante la página de descargas de su sitio web oficial (cf. http://www.sagemath.org). Está disponible para varias plataformas, entre ellas, las tres principales (Windows, macOs y Linux).

Así pues, para utilizar SageMath basta con disponer de un navegador y una conexión a internet. SageMathCell es apta para ejecutar programas que requieran de pocas líneas de código y escaso tiempo de ejecución. Para tareas más avanzadas es mucho más conveniente registrarse en CoCalc. Si es posible, no obstante, la mejor opción es contar con una instalación local.

Tanto en Cocalc como en una instalación local, son dos los modos fundamentales de uso de SageMath:

  • mediante una terminal, trabajando, como en Python, de forma interactiva;
  • mediante una aplicación web denominada Jupyter: se ejecuta en un navegador y permite la creación de documentos en los que se combinan celdas que contienen código, las salidas correspondientes, texto y gráficos.

Los documentos generados con Jupyter se denominan cuadernos (en inglés, notebooks) y son esencialmente ficheros en formato JSON. Son fácilmente exportables a otros formatos, como HTML, LaTeX, Markdown, etc.

Sintaxis

SageMath y Python

El lenguaje usado en SageMath es, en esencia, el lenguaje Python enriquecido con más funciones, clases y algún convenio de escritura adicional. Hasta la versión 8.9, SageMath se ha basado en Python 2. Desde la aparición de la versión 9.0 en enero de 2020, la distribución oficial de SageMath utiliza Python 3.

Dos son las diferencias de sintaxis más significativas entre Python y SageMath. En Python, el operador ^ es una disyunción exclusiva (operación XOR) realizada bit a bit; en SageMath, en cambio, se puede usar ^, además del operador **, para indicar potenciación. Del mismo modo, en Python, la expresión m/n, con m y n enteros, devuelve el resultado de la división, dando lugar a un número de tipo float. En SageMath, m/n proporciona la fracción irreducible equivalente. Veámoslo:

In [1]:
# Operaciones realizadas con la sintaxis de SageMath
print("5^3 =", 5^3)       # 5^3 es igual a 5**3
print("27/12 =", 27/12)
5^3 = 125
27/12 = 9/4
In [2]:
%%python3
# Ahora se usa la sintaxis de Python
print("5^3 =", 5^3)
print("27/12 =", 27/12)
5^3 = 6
27/12 = 2.25

En la celda precedente, la línea %%python es una de las denominadas funciones mágicas. Sirve para enviar directamente a IPython, el intérprete de Python, el resto de la celda. Con la orden %magic se puede obtener más información sobre estas funciones. Observemos asimismo que el resultado de 5^3 es el que cabía esperar: en binario, 5 y 3 son 101 y 011; luego 101 XOR 011 = 110, que es 6 en base 10.

Si se quiere que SageMath utilice estrictamente la sintaxis de Python y desactivar todo procesamiento previo del código, basta con ejecutar la orden preparser(False).

Clases y métodos

SageMath, al igual que Python, hace uso de la denominada programación orientada a objetos. En este tipo de programación se considera que cada elemento que manipula un programa es un objeto que pertenece a una clase determinada. La clase define la estructura de datos del objeto y las operaciones, llamadas métodos, que se pueden efectuar sobre él. Para aplicar un método mmm sobre un objeto ooo se escribe ooo.mmm(). Muchos métodos pueden actuar también como funciones y viceversa; en tales casos, las sintaxis mmm(ooo) y ooo.mmm() dan el mismo resultado.

Por ejemplo, en la celda siguiente, se asigna 27/12 a la variable a, que pasa a ser un objeto de la clase Rational de números racionales:

In [3]:
a = 27/12
type(a)
Out[3]:
<class 'sage.rings.rational.Rational'>

Los objetos de esta clase admiten, entre otros, los métodos numerator y denominator, que devuelven el numerador y el denominador de la fracción irreducible equivalente:

In [4]:
print("Numerador de a:", a.numerator())
print("Denominador de a:", a.denominator())
Numerador de a: 9
Denominador de a: 4

Ambos métodos pueden actuar también como funciones, luego se obtendría el mismo resultado con numerator(a) y denominator(a).

Tipos de datos

SageMath reconoce, por descontado, los tipos estándar de datos existentes en Python, en particular, int, float y complex. Pero SageMath cuenta con sus propios tipos de números, como la clase Rational de números racionales mencionada en el apartado anterior. Observa la diferencia entre Python y SageMath:

In [5]:
%%python3
# Tipos en Python
print("El tipo de 3 es ", type(3))
print("El tipo de 4.37 es ", type(4.37))
print("El tipo de 1+2j es ", type(1+2j))
El tipo de 3 es  <class 'int'>
El tipo de 4.37 es  <class 'float'>
El tipo de 1+2j es  <class 'complex'>
In [6]:
# Tipos en SageMath
print("El tipo de 3 es ", type(3))
print("El tipo de 4.37 es ", type(4.37))
print("El tipo de 1+2j es ", type(1+2j))
El tipo de 3 es  <class 'sage.rings.integer.Integer'>
El tipo de 4.37 es  <class 'sage.rings.real_mpfr.RealLiteral'>
El tipo de 1+2j es  <class 'sage.rings.complex_number.ComplexNumber'>

Hay más tipos de números, como los racionales, y diversas formas de representar como números en coma flotante los números reales y complejos (clases RealLiteral, Real Field, Real Double Field, etc.). No vamos a profundizar en ello. Simplificando y a efectos prácticos, cabe pensar que hay cuatro clases principales de números, representados por los símbolos ZZ, QQ, RR y CC, que corresponden a los números enteros, racionales, reales y complejos (estos dos últimos, en coma flotante). La ventaja de las clases de números de SageMath sobre los tipos de Python es que permiten trabajar con mayor precisión, admiten más métodos y cubren un rango más amplio de números.

Los símbolos ZZ, QQ, RR y CC se pueden usar, por ejemplo, para interrogar sobre la pertenencia de un número a uno de los conjuntos $\mathbb{Z}$, $\mathbb{Q}$, $\mathbb{R}$ y $\mathbb{C}$. Es lo que se hace en la celda siguiente para cada uno de los números de una lista:

In [7]:
numeros = [23/7, -1.87, 98, 3.2+5j, e^pi, sqrt(3)]
conjuntos = [("entero",ZZ), ("racional",QQ), ("real",RR), ("complejo",CC)]
for num in numeros:
    print("\nNúmero", num)
    for tipo,conj in conjuntos:
        if num in conj:
            print("Es", tipo)
        else:
            print("No es", tipo)
Número 23/7
No es entero
Es racional
Es real
Es complejo

Número -1.87000000000000
No es entero
Es racional
Es real
Es complejo

Número 98
Es entero
Es racional
Es real
Es complejo

Número 3.20000000000000 + 5.00000000000000*I
No es entero
No es racional
No es real
Es complejo

Número e^pi
No es entero
No es racional
Es real
Es complejo

Número sqrt(3)
No es entero
No es racional
Es real
Es complejo

En la celda precedente, se representan por e, pi y sqrt(3) los números $e$, $\pi$ y $\sqrt{3}$. Asimismo, la letra I designa la unidad imaginaria, que también se puede denotar por i. Observemos que un número complejo $a+bi$ se puede escribir, como en Python, en la forma a+bj, o bien en las formas a+b*i o a+b*I.

Los símbolos ZZ, QQ, RR y CC también se pueden usar, de forma análoga a int o float en Python, para convertir números de un tipo a otro, usando las clases específicas de SageMath. Por ejemplo, en la siguiente celda se obtiene una aproximación numérica del número $\pi$ con 10 cifras exactas, que luego se expresa en forma de fracción:

In [8]:
pi_aprox = numerical_approx(pi, digits=11)
pi_frac = QQ(pi_aprox)
print("Aproximación del número pi:", pi_aprox)
print("... expresada ahora como fracción:", pi_frac)
Aproximación del número pi: 3.1415926536
... expresada ahora como fracción: 1980127/630294

En la celda anterior, la aproximación numérica ha venido dada por la orden numerical_approx(). Para abreviar, se pueden usar sus sinónimos N() o n(); también se puede utilizar n() como método. En otras palabras, tanto numerical_approx(x) como N(x), n(x) o x.n() proporcionan el mismo resultado, esto es, un número decimal que aproxima el número x. El argumento opcional digits permite regular el grado de precisión deseado.

SageMath trata siempre de realizar los cálculos de forma exacta. Por eso, por ejemplo, usa aritmética racional para sumar fracciones o manipula de forma simbólica números como $\sqrt{7}$, dejándolos sin evaluar:

In [9]:
print(3/7 - 9/5 + 13/4)
print(9*sqrt(7) + sqrt(6.25)-6*sqrt(7))
263/140
3*sqrt(7) + 2.50000000000000

Si se quiere una expresión decimal de un número, ya hemos visto que se puede obtener con numerical_approx o sus sinónimos.

Rangos

La orden range de Python también funciona en SageMath. No obstante, puede ser un inconveniente que esta orden genere enteros de tipo int. Por ejemplo, la siguiente celda calcula los números menores que 200 que son un cuadrado perfecto:

In [10]:
[i for i in range(200) if ZZ(i).is_square()]
Out[10]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196]

Como el tipo int no admite el método is_square, se usa ZZ para convertir la variable a un entero de tipo Integer, propio de SageMath. En casos así es preferible utilizar la orden srange, que tiene la misma sintaxis y finalidad que range:

In [11]:
[i for i in srange(200) if i.is_square()]
Out[11]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196]

La orden srange admite otros tipos de datos y más argumentos:

In [12]:
print(srange(1/2, 5, 1/4))
print(srange(-3.5, 2, 1.5))
print(srange(10, 20, include_endpoint=True))
[1/2, 3/4, 1, 5/4, 3/2, 7/4, 2, 9/4, 5/2, 11/4, 3, 13/4, 7/2, 15/4, 4, 17/4, 9/2, 19/4]
[-3.50000000000000, -2.00000000000000, -0.500000000000000, 1.00000000000000]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]

Como sintaxis equivalente, en vez de

srange(inicio, fin, paso, include_endpoint=True)

se puede escribir

[inicio, inicio+paso..fin]

o bien

[inicio..fin, step=paso]

o simplemente [inicio..fin] si el paso es 1:

In [13]:
print([1/2, 3/4 .. 5])
print([-3.5, -2 .. 2])
print([-3.5 .. 2, step=1.5])
print([10..20])
[1/2, 3/4, 1, 5/4, 3/2, 7/4, 2, 9/4, 5/2, 11/4, 3, 13/4, 7/2, 15/4, 4, 17/4, 9/2, 19/4, 5]
[-3.50000000000000, -2.00000000000000, -0.500000000000000, 1.00000000000000]
[-3.50000000000000, -2.00000000000000, -0.500000000000000, 1.00000000000000]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]

Ayuda

SageMath cuenta con un sistema de autocompleción de órdenes y variables. Se escriben los primeros caracteres y a continuación se pulsa la tecla Tab. Aparece una ventana con todas las posibles opciones, tanto de órdenes como de variables. Basta entonces con seleccionar la que se desee. Por ejemplo, tras teclear numer y Tab, se abre una ventana con, al menos, las siguientes opciones: numerator, numerical_approx, numerical_eigenforms y numerical_integral.

La tecla Tab también permite escribir letras griegas o caracteres con acentos o flechas. Si se pulsa, por ejemplo, \betaTab, \GammaTab o a\hatTab, se obtiene β, Γ y â.

Para buscar información sobre una orden concreta, se escribe en una celda el nombre de la orden seguido del signo de cierre de interrogación, como, por ejemplo, numerical_approx?. Al evaluar la celda se muestra en una ventana la información correspondiente. Duplicando el signo de interrogación, como numerical_approx??, se accede además al código que define la orden:

In [14]:
numerical_approx?

Estas formas de obtención de ayuda también funcionan con los métodos aplicables a un objeto. Supongamos, por ejemplo, que la variable datos contiene una cadena de caracteres, resultado de una asignación como, digamos, datos="Nombre y DNI". Tras escribir datos. y pulsar Tab, se muestran todos los métodos disponibles: capitalize, center, count, etc. Si entonces se escribe datos.capitalize?, se accede a la información concreta sobre este método.

La orden search_src("término") busca todas las apariciones de término en los ficheros fuente de SageMath. Si se quiere, se puede abrir luego el fichero que pudiera resultar de interés. Análogamente, search_def("orden") devuelve una lista de los ficheros en los que orden está definida. Por ejemplo,

In [15]:
search_def("numerical_approx")

Aparecen varios ficheros. La siguiente celda muestra las últimas líneas de uno de ellos:

In [16]:
!tail -25 $SAGE_SRC/sage/misc/functional.py
        pass
    from sage.arith.all import factor
    from sage.structure.all import parent
    F = factor(x)
    n = parent(x)(1)
    for p, e in F:
        if e % 2:
            n *= p
    return n * F.unit()


def transpose(x):
    """
    Return the transpose of ``x``.

    EXAMPLES::

        sage: M = MatrixSpace(QQ,3,3)
        sage: A = M([1,2,3,4,5,6,7,8,9])
        sage: transpose(A)
        [1 4 7]
        [2 5 8]
        [3 6 9]
    """
    return x.transpose()

Impresión de resultados

La salida que genera una celda de código, que puede ser nula, es la que corresponde a la última orden de la celda. Para mostrar resultados intermedios, hay que usar la orden print, como hemos hecho hasta ahora, o la orden show, que intenta imprimir con la mayor calidad posible de acuerdo con la interfaz disponible. En el caso de expresiones matemáticas, casi siempre es preferible show, pues aplica el intérprete de LaTeX embebido en los cuadernos Jupyter. Comparemos:

In [17]:
print("(x,y)=", (sqrt(5),pi))
show("(x,y)=", (sqrt(5),pi))
print("Matriz A=\n", matrix([[1,2],[3,4]])) # \n genera un salto de línea
show("Matriz A=", matrix([[1,2],[3,4]]))
(x,y)= (sqrt(5), pi)
Matriz A=
 [1 2]
[3 4]

Para generar salidas complejas, al final resulta ineludible transformar los resultados en cadenas de caracteres y darles formato con los mecanismos de Python: el método format o las cadenas-f (f-strings). En el caso de la orden show, conviene además aplicar la orden html a toda la cadena, utilizar el lenguaje HTML y la notación matemática de LaTeX en el interior de la cadena y procesar los resultados de tipo matemático con la orden latex. Pongamos un par de ejemplos:

In [18]:
A = matrix([[1,2],[3,4]])
Ainv = A.inverse()
texto = f"La matriz inversa de $A={latex(A)}$ es $A^{{-1}}={latex(Ainv)}$."
show(html(texto))
La matriz inversa de es .
In [19]:
problema = """
    <strong>Problema:</strong><br>
    Halla las raíces del polinomio $x^2-3x+1$.
    """
show(html(problema))

sol = solve(x^2-3*x+1==0,x)
raices = [latex(x.subs(s)) for s in sol]
solucion ="""
    <em>Solución:</em><br>
    Las raíces son ${}$ y ${}$.
    """.format(*raices)
show(html(solucion))
Problema:
Halla las raíces del polinomio .
Solución:
Las raíces son y .

La función mágica %display latex facilita la presentación de resultados en notación matemática. Una vez incluida en una celda y evaluada, todas las salidas de las celdas de código son procesadas por LaTeX. De este modo, se evita tener que recurrir a la orden show con tanta frecuencia. Se anula el efecto de esta función con %display default. Lo comprobamos:

In [20]:
%display latex   # activamos el uso de LaTeX
sqrt(3)^5
Out[20]:
In [21]:
%display default  # LaTeX desactivado
sqrt(1000)
Out[21]:
10*sqrt(10)
In [22]:
%display latex  # recuperamos el uso de LaTeX
arcsin(1)       # LaTeX sigue activo
Out[22]:

Funciones

Sagemath proporciona directamente, sin necesidad de importar módulos, un gran número de funciones predefinidas. Es el caso de las funciones matemáticas habituales: floor, ceil, round, abs, exp, log, sin, cos, tan, arcsin, arccos, arctan, sinh, cosh, etc. Maticemos que log(x) es el logaritmo neperiano de x, mientras que log(x,10) es su logaritmo decimal. Aunque nada impide usar las funciones del módulo math, conviene saber que las funciones predefinidas de SageMath aportan mayor exactitud, adaptación a las clases de números de SageMath y la posibilidad de actuar como métodos, como pone de manifiesto la celda siguiente:

In [23]:
from math import sin as seno
a = pi/4
print(sin(a), a.sin(), sin(a).n())
print(seno(a)) # seno(a) es válido, a.seno() da error
1/2*sqrt(2) 1/2*sqrt(2) 0.707106781186548
0.7071067811865475

Si se necesitan más funciones, se pueden importar de aquellos módulos en los que pudieran venir dadas, o bien se pueden definir, al igual que en Python estándar, mediante las órdenes def, para las funciones regulares, y lambda, para las funciones anónimas.

Demos varios ejemplos. Usemos primero una función anónima para generar una matriz de Hilbert de orden $7$, esto es, una matriz $7\times7$ cuyo elemento $(i,j)$ es $i/j$:

In [24]:
m = matrix(7, 7, lambda i,j:(i+1)/(j+1))
show(m)

Definimos seguidamente una función para evaluar la función matemática $$ f(x,y) = \frac{1+x^5+y^5}{x^4+y^4}\operatorname{sen}(x^3-y^3) $$ y se representan sus curvas de nivel. Se usa una función anónima para excluir del gráfico una región en la que esta función no está definida o toma valores excesivamente altos:

In [25]:
def f(x,y):
    return (1+x^5+y^5)*sin(x^3-y^3)/(x^4+y^4)

contour_plot(f, (-3,3), (-3,3), 
             region=lambda x,y: x^2 + y^2 - 0.12,
             plot_points=350, contours=20, cmap="jet", colorbar=True)
Out[25]:

Pongamos un último ejemplo. La función primos_hasta calcula, de un modo no totalmente eficiente, todos los números primos menores que uno dado mediante la criba de Eratóstenes:

In [26]:
def primos_hasta(num, verboso=False):
    """
    Obtención de todos los números primos menores que un número num
    mediante la criba de Eratóstenes.
    Entrada: num (número entero positivo) y verboso (variable booleana). 
             Si verboso=True, se imprimen los cálculos intermedios.
    Salida: Lista de los números primos menores que num.
    """
    num_inicial = num
    if verboso and num%5!=0:
        # Ajuste de num al siguiente múltiplo de 5
        # para facilitar la impresión de resultados
        num = 5 * (num//5+1)
    es_primo = num*[True]
    es_primo[0] = es_primo[1] = False
    j = 2
    while j**2 <= num:
        if es_primo[j]:
            for k in range(j**2,num,j):
                es_primo[k] = False
            if verboso:
                print("\nIteración j=", j)
                cuadro = [[ [5*i+k, es_primo[5*i+k]] for k in range(5)] 
                          for i in range(num//5)]
                print(table(cuadro))
        j += 1
    return [j for j in range(num_inicial) if es_primo[j]]

Una vez ejecutado el código de la celda precedente, podemos usar primos_hasta para hallar, por ejemplo, los números primos menores que 48, imprimiendo los resultados intermedios:

In [27]:
num = 48
print("Cálculo de los números primos menores que", num)
primos = primos_hasta(num, verboso=True)
print("\nResultado:\n", primos)
Cálculo de los números primos menores que 48

Iteración j= 2
  [0, False]    [1, False]    [2, True]     [3, True]     [4, False]
  [5, True]     [6, False]    [7, True]     [8, False]    [9, True]
  [10, False]   [11, True]    [12, False]   [13, True]    [14, False]
  [15, True]    [16, False]   [17, True]    [18, False]   [19, True]
  [20, False]   [21, True]    [22, False]   [23, True]    [24, False]
  [25, True]    [26, False]   [27, True]    [28, False]   [29, True]
  [30, False]   [31, True]    [32, False]   [33, True]    [34, False]
  [35, True]    [36, False]   [37, True]    [38, False]   [39, True]
  [40, False]   [41, True]    [42, False]   [43, True]    [44, False]
  [45, True]    [46, False]   [47, True]    [48, False]   [49, True]

Iteración j= 3
  [0, False]    [1, False]    [2, True]     [3, True]     [4, False]
  [5, True]     [6, False]    [7, True]     [8, False]    [9, False]
  [10, False]   [11, True]    [12, False]   [13, True]    [14, False]
  [15, False]   [16, False]   [17, True]    [18, False]   [19, True]
  [20, False]   [21, False]   [22, False]   [23, True]    [24, False]
  [25, True]    [26, False]   [27, False]   [28, False]   [29, True]
  [30, False]   [31, True]    [32, False]   [33, False]   [34, False]
  [35, True]    [36, False]   [37, True]    [38, False]   [39, False]
  [40, False]   [41, True]    [42, False]   [43, True]    [44, False]
  [45, False]   [46, False]   [47, True]    [48, False]   [49, True]

Iteración j= 5
  [0, False]    [1, False]    [2, True]     [3, True]     [4, False]
  [5, True]     [6, False]    [7, True]     [8, False]    [9, False]
  [10, False]   [11, True]    [12, False]   [13, True]    [14, False]
  [15, False]   [16, False]   [17, True]    [18, False]   [19, True]
  [20, False]   [21, False]   [22, False]   [23, True]    [24, False]
  [25, False]   [26, False]   [27, False]   [28, False]   [29, True]
  [30, False]   [31, True]    [32, False]   [33, False]   [34, False]
  [35, False]   [36, False]   [37, True]    [38, False]   [39, False]
  [40, False]   [41, True]    [42, False]   [43, True]    [44, False]
  [45, False]   [46, False]   [47, True]    [48, False]   [49, True]

Iteración j= 7
  [0, False]    [1, False]    [2, True]     [3, True]     [4, False]
  [5, True]     [6, False]    [7, True]     [8, False]    [9, False]
  [10, False]   [11, True]    [12, False]   [13, True]    [14, False]
  [15, False]   [16, False]   [17, True]    [18, False]   [19, True]
  [20, False]   [21, False]   [22, False]   [23, True]    [24, False]
  [25, False]   [26, False]   [27, False]   [28, False]   [29, True]
  [30, False]   [31, True]    [32, False]   [33, False]   [34, False]
  [35, False]   [36, False]   [37, True]    [38, False]   [39, False]
  [40, False]   [41, True]    [42, False]   [43, True]    [44, False]
  [45, False]   [46, False]   [47, True]    [48, False]   [49, False]

Resultado:
 [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

En realidad, como los números primos son tan relevantes en las matemáticas, SageMath ya cuenta con órdenes para analizarlos, como is_prime o prime_range. La celda siguiente muestra varios modos de obtener el mismo resultado que primos_hasta, aunque con distintos grados de eficiencia:

In [28]:
num = 2000
lista_1 = primos_hasta(num)
lista_2 = [j for j in range(num) if is_prime(j)]
lista_3 = list(filter(is_prime, range(num)))
lista_4 = prime_range(num)
# Comprobamos que todas las listas son iguales
lista_1 == lista_2 == lista_3 == lista_4
Out[28]:

Es posible que a lo largo de una sesión con SageMath se acabe perdiendo el significado de alguna variable o función predefinida. Por ejemplo, tras las asignaciones i = 5 y cos = [1, 2, 3], la variable i deja de representar la unidad imaginaria, mientras que cos ya no es la función coseno, sino una variable que almacena una lista.

La orden restore() recupera el valor original de las variables y funciones predefinidas. La orden reset() es más radical, pues además borra todas las variables y definiciones introducidas por el usuario. Si sólo se quieren restaurar unas variables o funciones concretas, digamos aaa, bbb y ccc, se emplea la orden reset("aaa, bbb, ccc"). Las dos siguientes celdas ilustran la utilización de restore y reset:

In [29]:
mi_var = 100           # variable introducida por el usuario
pi = 25                # se pierde el valor de pi
cos = lambda x: x^2    # cos ya no es la función coseno
print(cos(pi), mi_var)
restore()              # se recupera el significado de pi y cos
print(cos(pi), mi_var) # ... sin afectar a la variable mi_var
625 100
-1 100
In [30]:
def fun(x): return 2*x   # función definida por el usuario
e = 5                    # se pierde el valor de e
def log(x): return x^2   # log ya no es la función logaritmo neperiano
print(log(e), fun(e)) 
reset()                  # recuperación de e y log, borrado de fun
print(log(e))
#print(fun(e))            # Causa un error: fun ya no está definida
25 10
1

Manipulación simbólica y algebraica

Expresiones simbólicas

Decíamos anteriormente que SageMath es capaz de manipular números de forma simbólica al objeto de realizar cálculos de forma exacta. Veamos, de hecho, qué tipo de dato asocia SageMath a pi o sqrt(3):

In [31]:
print(type(sqrt(3)))
print(type(pi))
<class 'sage.symbolic.expression.Expression'>
<class 'sage.symbolic.expression.Expression'>

Así pues, tanto pi como sqrt(3) pertenecen a la clase de expresiones simbólicas, representada por SR (del inglés Symbolic expression Ring). A esta clase también pertenecen todas las expresiones que dependan de una o más indeterminadas o variables simbólicas. La letra x es, por omisión, una de estas variables. Si se necesitan más, hay que declararlas con la orden var. Los nombres de las variables simbólicas han de ser expresiones válidas como variables de Python (son legales mivar, miVar, mi_var, mi2var, etc.; no lo es 2mivar).

Pongamos algunos ejemplos. La celda siguiente muestra cómo factorizar un polinomio y activa las salidas con LaTeX, que serán ahora más adecuadas:

In [32]:
%display latex          
factor(2*x^3-x^2-7*x+6) # o bien (2*x^3-x^2-7*x+6).factor()
Out[32]:

Mostramos a continuación cómo declarar un par de variables simbólicas nuevas y cómo expandir una expresión algebraica:

In [33]:
var("y,z")
expand((y-z)^4)   # o bien ((y-z)^4).expand()
Out[33]:

Una vez declarada una variable, se puede seguir utilizando en las celdas posteriores. Descomponemos ahora el cociente $\frac{y^2 - y + 4}{y^2 - 1}$ en fracciones simples :

In [34]:
p = (y^2-y+4) / (y^2-1)
p.partial_fraction()
Out[34]:

Podemos usar las expresiones simbólicas para verificar identidades matemáticas, como $$\cos 3 z= \cos^3 z-3 \cos z \operatorname{sen}^2 z.$$ Podemos comprobarlo de este modo:

In [35]:
bool(cos(3*z) == cos(z)^3 - 3*cos(z)*sin(z)^2)
Out[35]:

También podríamos ver que es nula la diferencia entre los dos miembros de la identidad:

In [36]:
(cos(z)^3 - 3*cos(z)*sin(z)^2 - cos(3*z)).simplify_trig()
Out[36]:

Además de manipulaciones algebraicas o trigonométricas, se pueden hacer muchos otros cálculos con las expresiones simbólicas. En las celdas siguientes, por ejemplo, se integra la función $e^x\cos 2x$ y se halla su derivada tercera:

In [37]:
fun = exp(x)*cos(2*x)
integral(fun,x)
Out[37]:
In [38]:
derivative(fun,x,3)
Out[38]:

Sustituciones

No se pueden hacer asignaciones a las variables simbólicas. Si se escribiese x = 10, por ejemplo, x dejaría de ser una variable simbólica y pasaría a ser una variable Python normal que almacena el valor 10. Si se quisiera volver a usar x como variable simbólica, habría que declararla de nuevo con var o recuperarla con restore. ¿Qué hacer entonces para hallar el valor de una expresión simbólica cuando las variables que en ella intervienen han de tomar valores concretos? Lo que se hace es una sustitución, pasando los valores de forma adecuada.

Pongamos un ejemplo concreto. Consideremos la siguiente expresión simbólica:

In [39]:
var("x,y,z")
f = x + y^2 + z^3

Supongamos que queremos evaluar f cuando x, y y z toman los valores 1, 2 y 3, respectivamente. Cada una de las siguientes celdas contiene un par de modos lícitos de hacerlo:

In [40]:
print(f.subs(x=1, y=2, z=3), f(x=1, y=2, z=3))
32 32
In [41]:
print(f.subs({x:1, y:2, z:3}), f({x:1, y:2, z:3}))
32 32
In [42]:
valores = dict(x=1, y=2, z=3)
print(f.subs(**valores), f(**valores))
32 32
In [43]:
valores = {x:1, y:2, z:3}
print(f(valores), f.subs(valores))
32 32

Funciones simbólicas

Las funciones definidas con la sintaxis de Python se pueden evaluar en variables o expresiones simbólicas, dando lugar a expresiones simbólicas que se pueden manipular como cualquier otra. Por ejemplo, en la siguiente celda se define una función $f$ con la orden def:

In [44]:
def f(t):
    return (2*t^2-t+4) / (3*t^2-1)

Observemos que $f$ es una mera función Python:

In [45]:
type(f)
Out[45]:

Pero, como x es una variable simbólica, f(x) es una expresión simbólica. Aprovechémoslo, por ejemplo, para calcular la derivada de $f$:

In [46]:
derivative(f(x),x).rational_simplify()
Out[46]:

Y también el límite de $f$ cuando $x\to+\infty$:

In [47]:
limit(f(x),x=+oo)
Out[47]:

SageMath permite definir funciones imitando la notación matemática habitual. Por ejemplo,

In [48]:
g(x) = (x^2-x+4) / (x^2-1)   # definimos g
g                            # ... y la mostramos
Out[48]:

Las funciones así definidas se denominan funciones simbólicas. Se pueden evaluar, integrar, representar gráficamente, etc., como ponen de manifiesto las celdas siguientes:

In [49]:
# Evaluación de g en 5, 6, ..., 10
for j in range(5,10):     
    print("g(",j,")=", g(j))
g( 5 )= 1
g( 6 )= 34/35
g( 7 )= 23/24
g( 8 )= 20/21
g( 9 )= 19/20
In [50]:
# Integral de g en el intervalo [3,9]
integrate(g(x),(x,3,9))
Out[50]:
In [51]:
# Representación gráfica de g
plot(g(x), (x,-3,3), ymin=-10, ymax=10, detect_poles='show')
Out[51]:

También se pueden definir simbólicamente, evaluar, manipular y representar funciones multivariadas:

In [52]:
h(x,y) = (2*x+3*y^2)/(x^2+y^2+1)^2  # definición de h
print("h(2,3)=", h(2,3))
print("h(-1,1)=", h(-1,1))
contour_plot(h(x,y), (x,-1.5,1.5), (y,-1.5,1.5), labels=True) # curvas de nivel de h
h(2,3)= 31/196
h(-1,1)= 1/9
Out[52]:

Finalmente, SageMath puede manejar funciones simbólicas abstractas, que se declaran con la función function. A modo de ejemplo, en la celda siguiente se introducen dos funciones simbólicas a partir de las cuales se define una tercera:

In [53]:
function("f,g")       # f y g son funciones simbólicas abstractas
h(x) = f(x) * g(x)    # h es una función simbólica definida a partir de f y g
h                     # mostramos h
Out[53]:

SageMath conoce la regla de derivación de un producto de funciones (aunque la notación no sea del todo correcta):

In [54]:
h(x).derivative()        # derivada de h
Out[54]:

Sustituimos f y g por funciones concretas y observamos en qué se convierte h:

In [55]:
h.subs({f(x):x^2, g(x):log(x)})
Out[55]:

Podemos incluso evaluar h en este caso:

In [56]:
h(x).subs({f(x):x^2, g(x):log(x)}).subs(x=e)
Out[56]:

Hipótesis

En ocasiones SageMath no transforma o simplifica una determinada expresión porque carece de la información necesaria para hacerlo:

In [57]:
sqrt(x^2)
Out[57]:

Si precisamos que x representa un número real positivo o negativo, entonces SageMath realiza la simplificación correcta:

In [58]:
forget(); assume(x>0)
print("sqrt(x^2)=", sqrt(x^2), "si x>0")
forget(); assume(x<0)
print("sqrt(x^2)=", sqrt(x^2), "si x<0") 
forget()
sqrt(x^2)= x si x>0
sqrt(x^2)= -x si x<0

Con forget se borra cualquier hipótesis previa que se haya podido realizar (es innecesario si no se ha efectuado ninguna). Con assume se le indica a SageMath que manipule las expresiones siguientes de acuerdo con la hipótesis que figura en su argumento.

Ecuaciones y sistemas no lineales

SageMath dispone de la orden solve para resolver algebraicamente ecuaciones y sistemas no lineales. Hallemos, por ejemplo, las raíces del polinomio $p(x)=x^6 - 9x^5 + 12x^4 + 76x^3 - 144x^2 - 192x + 256$:

In [59]:
p = x^6 - 9*x^5 + 12*x^4 + 76*x^3 - 144*x^2 - 192*x + 256
solve(p==0, x)
Out[59]:

Cuando el segundo miembro de la ecuación es $0$, se puede omitir éste. Luego hubiera bastado con solve(p, x) para obtener el mismo resultado. Observemos, por otra parte, que, como $p$ es un polinomio de grado $6$, deberíamos haber obtenido $6$ raíces. Lo que sucede es que algunas raíces son múltiples. Para saber la multiplicidad de cada una se usa el argumento opcional multiplicities:

In [60]:
solve(p==0, x, multiplicities=True)
Out[60]:

Queda ahora claro que $1$ es una raíz simple de $p$, $-2$ es una raíz doble y $4$ es una raíz triple. Si se prefiere expresar las soluciones mediante diccionarios, se añade el argumento solution_dict:

In [61]:
solve(p==0, x, solution_dict=True)
Out[61]:

Una vez resuelta una ecuación, lo habitual es utilizar el valor de una o más soluciones. Para extraer este valor, lo más simple es usar el método subs:

In [62]:
soluciones = solve(p==0, x)   
x.subs(soluciones[1])  # extracción del valor de x de la segunda solución
Out[62]:
In [63]:
soluciones = solve(p==0, x)
raices = [x.subs(sol) for sol in soluciones]
raices   # lista de todas las raíces de p
Out[63]:

En la medida de lo posible, solve tiene en cuenta las hipótesis que hayan podido hacerse:

In [64]:
forget()             # borrado preventivo de toda hipótesis
assume(x>0)          # imponemos la restricción x>0
show(solve(p==0, x)) # sólo se devuelven las raíces positivas de p
forget()             # anulamos la hipótesis para que no influya en otros cálculos

La orden solve puede trabajar con ecuaciones puramente simbólicas:

In [65]:
var("a,b,c")
solve(a*x^2 + b*x + c == 0, x)
Out[65]:

Veamos ahora algunos ejemplos de sistemas de ecuaciones. Hallemos los puntos de intersección de la circunferencia $x^2+y^2=5$ y la recta $y=2x-1$:

In [66]:
solve([x^2+y^2==5, y==2*x-1], x, y)
Out[66]:

Vemos que hay dos puntos de intersección. En este caso las soluciones se expresan de forma exacta. No siempre es posible. Con frecuencia hay que contentarse con una aproximación numérica, como muestra el resultado de la siguiente celda, en la que se calculan los puntos de corte de la hipérbola $x^2-3x-y^2=1$ y la circunfencia $x^2-2x+y^2-y=6$:

In [67]:
solve([x^2-3*x-y^2==1, x^2-2*x+y^2-y==6], x, y)
Out[67]:

La siguiente celda presenta las soluciones de una forma seguramente más clara:

In [68]:
soluciones = solve([x^2-3*x-y^2==1, x^2-2*x+y^2-y==6], x, y)
puntos = [(x.subs(sol),y.subs(sol)) for sol in soluciones]
for punto in puntos:
    show(punto)

En el código precedente, para evitar la repetición de subs, se puede crear primero un vector de componentes x e y y luego efectuar la sustitución:

In [69]:
soluciones = solve([x^2-3*x-y^2==1, x^2-2*x+y^2-y==6], x, y)
puntos = [vector([x,y]).subs(sol) for sol in soluciones]
for punto in puntos:
    show(punto)

La orden solve tiene más opciones y se puede aplicar también a algunas ecuaciones con funciones trascendentes, como las funciones trigonométricas. Es muy recomendable ejecutar solve? en una celda y leer la documentación que aparece entonces.

Celdas de configuración. La evaluación de las dos celdas siguientes cambia el formato por omisión de este cuaderno.
In [70]:
# Se desactiva primero la acción de %display latex, si estuviera activa
%display default
In [71]:
%%html
<style>
h1{text-align: center; color: rgb(185,74,72);}
h2{text-align: center; color: rgb(0,102,0); padding: 0.25em 0;
   border: 2px solid rgb(0,191,0); border-width: 2px 0;}
h3{border-bottom: 2px solid rgb(153,153,153);} 
h4{color: rgb(58,135,173); font-size: 115%!important;
   font-weight: bold!important;}
.text_cell_render{font-family: "Trebuchet MS",Geneva,sans-serif;
                  font-size: 110%; line-height: 1.5;}
.MathJax_Display{margin: 0.5em 0;}
</style>
Realizado por Juan José Torrens para la asignatura de Cálculo II
Grados de Ciencias y de Ciencia de Datos
Universidad Pública de Navarra
Última versión: 14-1-2021