3 Primeros Pasos en Julia

Julia es un lenguaje de programación dinámico de alto nivel y altamente eficiente diseñado para realizar análisis numérico. Julia fue creado para eliminar el problema del doble lenguaje.

Los lenguajes de programación se categorizan en lenguajes de alto nivel y lenguajes de bajo nivel.

Un lenguaje de programación de alto nivel se caracteriza por expresar el algoritmo de una manera adecuada a la capacidad cognitiva humana, en lugar de la capacidad ejecutora de las máquinas.

Un lenguaje de programación de bajo nivel es aquel en el que sus instrucciones ejercen un control directo sobre el hardware y están condicionados por la estructura física de las computadoras que lo soportan.

El uso de la palabra bajo nivel no implica que el lenguaje sea menos potente que un lenguaje de alto nivel, sino que se refiere a la reducida abstracción entre el lenguaje y el hardware

Wikipedia

Los lenguajes de bajo nivel (como código binario y Assembly) requieren más tiempo y esfuerzo al programar pero son bastante eficientes en la ejecución.

Entre los lenguajes de alto nivel, existen aquellos que son estáticos y fuertemente enfocados a los tipos de objetos (como C++, Fortran, etc) y aquellos que son interpretados y dinámicos (como Matlab, R, Python, etc). Todos ellos buscan minimizar el tiempo de programación respecto a los lenguajes de bajo nivel. Los lenguajes de alto nivel estáticos son más eficientes porque permiten generar código binario de forma óptima en la compilación de los programas. Su desventaja es que requieren de más esfuerzo y cuidado al escribir el código. Por el contrario, los lenguajes de alto nivel dinámicos directamente ejecutan las instrucciones sin previamente haber sido compilados a código binario. Los anterior hace que estos son menos eficientes en lo que se refiere al tiempo de ejecución que los lenguajes de alto nivel estáticos. Su ventaja es que el código es muy fácil de escribir y precido bastante a nuestro lenguaje.

El problema del doble lenguaje surge en dos situaciones:

  1. Realizar el prototipo de programa en un lenguaje de alto nivel dinámico y luego re-programar en un lenguaje de alto nivel estático para obtener mayor velocidad en ejecución.
  2. Muchos lenguajes de alto nivel dinámico (incluidos Matlab, Python y R) tienen dos capas: lenguaje de alto nivel dinámico en la interacción con el usuario que llama a programas en lenguaje de alto nivel estático para que se ejecuten detrás de bambalinas.

Para instalar Julia, bajar el archivo correspondiente al sistema operativo usado desde el sitio oficial de Julia: https://julialang.org/downloads/. Para contar con un entorno de desarrollo integrado (IDE) para trabajar con Julia hay varias opciones: (1) Visual Studio Code + Julia: Bajar VSCode (https://code.visualstudio.com/) e instalar la extensión Julia (Extensiones + Instalar Julia). Este es el IDE que uso. (2) Atom + Juno: Bajar Atom (https://atom.io/) e instalar JUNO (Configuración + Intalar Paquetes). De acuerdo a la última versión de JuliaCon, los desarrolladores de un IDE para Julia están ahora concentrando todos sus esfuerzos en la extensión de Visual Studio Code. También es posible utilizar los entornos notebook de Jupyter con Julia, para lo cual se requiere instalar el paquete IJulia en Julia (ver más adelante sobre instalación de paquetes).

Alternativamente, se puede instalar un empaquetado de aplicaciones y paquetes (Julia + IDE + Jupyter + selección de paquetes) llamado JuliaPRO y creado por Julia Computing. La versión básica es gratuita y sólo requiere registro de usuario. Se puede descargar JuliaPRO del siguiente link: https://juliacomputing.com/products/juliapro.html.

3.1 Interactuando con Julia

La versión de linea de comandos de Julia (REPL):

REPL

Usando VSCode como interfaz IDE:

IDE

Usando Jupyter notebooks

Jupyter

Julia usa el espacio de un directorio como área de trabajo. Dicho directorio se denomina directorio de trabajo y todos los archivos (que no formen parte del paquete) deber estar en dicho directorio (por ejemplo, datos, procedimientos adicionales, etc).

  • Usar pwd() para conocer el directorio de trabajo actual.
  • Usar cd("directorio") para cambiar el directorio de trabajo.
pwd()
"/Users/mauriciotejada/Dropbox/Teaching/MAE - Julia/Nootebooks"
cd("/Users/mauriciotejada/Dropbox/")
pwd()
"/Users/mauriciotejada/Dropbox"
cd("/Users/mauriciotejada/Dropbox/Teaching/MAE - Matlab/Nootebooks Julia")
pwd()
IOError: chdir /Users/mauriciotejada/Dropbox/Teaching/MAE - Matlab/Nootebooks Julia: no such file or directory (ENOENT)



Stacktrace:

 [1] uv_error at ./libuv.jl:85 [inlined]

 [2] cd(::String) at ./file.jl:76

 [3] top-level scope at In[3]:1

Note: Los usuarios de Windows deben usar las rutas en formato C:\Dir\Subdir\

3.2 Ayuda

Más allá de este curso introductorio, la mejor forma de aprender Julia es a través de la experimentación. Las dos fuentes más importantes para referencia: 1. Documentación de Julia: https://docs.julialang.org/en/v1.1/. 2. Google (existen literalmente millones de códigos en la web que resuelven distintos problemas).

Para acceder a la documentación de Julia usar ? y el curso cambiará a help?>. Por ejemplo, se puede buscar ayuda sobre un comando o procedimiento en particular usando ? comando

? println
  println([io::IO], xs...)

  Print (using print) xs followed by a newline. If io is not supplied, prints
  to stdout.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> println("Hello, world")
  Hello, world

  julia> io = IOBuffer();

  julia> println(io, "Hello, world")

  julia> String(take!(io))
  "Hello, world\n"

3.3 Paquetes

Es posible aumentar las capacidades de Julia usando paquetes (ya sea con los que vienen en la instalación base como con externos). Los paquetes son invocados (después de haber sido instalados) usando la sintaxis: using nombre_paquete. Para administrar los paquetes en julia se usa el paquete Pkg. La sintaxis para instalar un paquete es:

using Pkg
Pkg.add("nombre_paquete")

Nota: El computador debe estar conectado a Internet para descargar los archivos binarios del paquete a instalar. Los archivos descargados son guardados en la carpeta: ~/.julia/environments/.

Cada paquete por defecto instala todas las dependencias necesarias para funcionar (esto es, otros paquetes necesarios).

Adicionalmente, los siguientes comandos son útiles:

  • Pkg.rm("nombre_paquete"): Elimina un paquete.
  • Pkg.build("nombre_paquete"): Reconstruye un paquete desde sus archivos binarios.
  • Pkg.status(): Provee una lista de los paquetes instalados.
  • Pkg.update(): Actualiza los paquetes instalados.

Los paquetes (y sus versiones) utilizados en este curso son:

Pkg.status()
Status `~/.julia/environments/v1.5/Project.toml`
  [336ed68f] CSV v0.8.2
  [49dc2e85] Calculus v0.5.1
  [667455a9] Cubature v1.5.1
  [31c24e10] Distributions v0.23.8
  [c04bee98] ExcelReaders v0.11.0
  [2fe49d83] Expectations v1.6.0
  [f6369f11] ForwardDiff v0.10.12
  [60bf3e95] GLPK v0.14.3
  [7073ff75] IJulia v1.23.0
  [a98d9a8b] Interpolations v0.13.0
  [b6b21f68] Ipopt v0.6.5
  [4138dd39] JLD v0.10.0
  [4076af6c] JuMP v0.21.5
  [b964fa9f] LaTeXStrings v1.2.0
  [2774e3e8] NLsolve v4.4.1
  [429524aa] Optim v0.21.0
  [91a5bcdd] Plots v1.9.1
  [d330b81b] PyPlot v2.9.0
  [1fd47b50] QuadGK v2.4.1
  [fcd29c91] QuantEcon v0.16.2
  [f2b01f46] Roots v1.0.6
  [24249f21] SymPy v1.0.34

3.4 Comentarios

Comentar o dejar documentación en diferentes partes de un código es muy útil para explicar en detalle que es exactamente qué cálculos se están realizando. Esto es particularmente útil cuando el código es compartido con otra persona o con uno mismo en el futuro.

  • Comentarios de una sola línea inician con #. Todo lo que precede a este símbolo será ignorado por Julia.
  • Comentarios de más de una línea inician con #= y cierran con =#. Estos comentarios son útiles para escribir documentación detallada que requiera de varias líneas.

3.5 Variables

Julia es un lenguaje basado en expresiones.

Las expresiones que introducimos mediante el teclado son interpretadas y evaluadas.

Cada expresión genera algún tipo de output que puede ser asignado en una variable. La asignación se realiza de la siguiente forma:

nombrevariable = expresión

Si se introduce alguna expresión pero no se define un nombre para la variable, Julia asigna la expresión a una variable auxiliar temporal denominada ans.

Todas las variables son almacenadas en la memoria RAM del computador por lo que se pierden en cuanto uno sale y apaga Julia.

Algunas reglas al elegir los nombres de las variables:

  • Los nombres deben empezar con una letra y pueden contener letras, números y _.
  • Julia es sensible a mayúsculas y minúsculas (x es diferente de X).
  • Julia soporta el sistema unicode para nombrar variables. Por ejemplo, es posible llamar una variable \alpha<TAB> = 0.3 (recuerde presionar la tecla TAB antes de escribir el signo igual).

Tipos básicos de variables:

  1. Numéricas (distinguiendo entre enteros (Int64), decimales (Float64) y complejos (Complex)):
x = 2.5
typeof(x) # función que muestra el tipo de la variable
Float64
y = 4
typeof(y)
Int64
variable_imaginaria = 2.0 + 5.0im
typeof(variable_imaginaria)
Complex{Float64}
α = 0.4
typeof(α)
Float64
  1. Texto (se asignan usando "" o """ """):
Y_1 = "Texto"
typeof(Y_1)
String
var_texto = "Pueden incluirse Espacios"
"Pueden incluirse Espacios"
var_texto_muy_largo = """
Esto es un texto muy largo incluido en una
variable. El texto tiene varias lineas.
"""
"Esto es un texto muy largo incluido en una\nvariable. El texto tiene varias lineas.\n"
  1. Booleanos (variables dicotómicas o que sólo admiten dos valores: verdadero (true) o falso (false).)
var_bool = true
typeof(var_bool)
Bool
  1. Carácter (el valor de una constante de tipo carácter es el valor numérico de ese carácter en el código ASCII):
var_car = 'a'
typeof(var_car)
Char
Int(var_car) # para encontrar el valor numérico del carácter.
97

Para saber qué objetos se han creado se usan los comandos varinfo(). Esta función muestra los nombres de todas las variables y da información detallada del tipo y el tamaño.

varinfo()
name size summary
Base Module
Core Module
Main Module
Y_1 13 bytes String
var_bool 1 byte Bool
var_car 4 bytes Char
var_texto 33 bytes String
var_texto_muy_largo 91 bytes String
variable_imaginaria 16 bytes Complex{Float64}
x 8 bytes Float64
y 8 bytes Int64
α 8 bytes Float64

3.6 Operaciones Matemáticas Básicas

Julia tiene todas las operaciones matemáticas básicas:

  1. Suma (+)
  2. Resta (-)
  3. Multiplicación (*)
  4. División (/)
  5. Potencia (^)
  6. Módulo (%)
x = 2.4
y = 5
@show z_sum = x + y;
z_sum = x + y = 7.4

Una forma muy útil de presentar los resultados, tanto texto como resultados, es usando la macro @show. Una macro recibe una pieza de código como insumo (una expresión) y genera otra pieza de código como resutlados (una expresión diferente). En palabras simples son funciones que modifican el código previo a ser compilado y son parte del paradigma de programación funcional.

typeof(z_sum)
Float64
@show z_pro = x*y;
z_pro = x * y = 12.0
@show z_pot = x^y;
z_pot = x ^ y = 79.62623999999998
x = 5
y = 2
@show  z_mod = y % x;
z_mod = y % x = 2

La operación multiplicación * esta definida también cuando las variables son tipo texto y sirve para concatenar variables:

a = "hola amigo "
b = "Jon Snow"
@show z = a * b;
z = a * b = "hola amigo Jon Snow"

3.7 Estructura de Datos

En el trabajo diario con variables es útil poder almacenarlas conjuntamente en ciertas estructuras de datos y trabajar con todas ellas al mismo tiempo que usarlas todas de manera dispersa. Los tipos de estructuras de datos básicos son:

  1. Tuplas (Tuples)
  2. Diccionarios (Dictionaries)
  3. Arreglos (Arrays)

Algunos comentarios:

  • Las Tuplas y los Arreglos son secuencias ordenadas de elemento y por tanto es posible utilizar índices para describir sus elementos. Los Diccionarios, en tanto, asocian a cada elemento una clave.
  • Los Diccionarios y los Arreglos son mutables (podemos cambiar sus elementos), en tanto que las Tuplas no lo son.
  • Las tres estructuras de datos pueden contener una mezcla de tipos de variables, por ejemplo almacenando tanto valores numéricos con de tipo texto.
  1. Las Tuplas se crean utilizando el siguiente sintaxis: nombre = (item1, item2, ...)
xtup = (1, 2, 3, "texto")
(1, 2, 3, "texto")

Cada elemento puede ser recuperado usando el índice asociado a su ordenamiento (empezando en 1) bajo el sintaxis nombre_var[indice]:

xtup[4]
"texto"

Como las Tuplas son inmutables, no se puede cambiar el contenido de ellas una vez que ya se crearon.

xtup[2] = 8
MethodError: no method matching setindex!(::Tuple{Int64,Int64,Int64,String}, ::Int64, ::Int64)


Stacktrace:

 [1] top-level scope at In[25]:1

Las tuplas también pueden contener nombres o identificadores para cada elementos:

xtupn = (num1 = 1, num2 = 2, num3 = 3, texto1 = "texto")
@show xtupn;
xtupn = (num1 = 1, num2 = 2, num3 = 3, texto1 = "texto")

En este caso cada elemento puede ser recuperado usando el nombre o identificador asociado (el orden en este caso es irrelevante). Hay dos formatos para realizar esto: (1) usando la notación tupla.nombre y (2) usando la notación tupla[:nombre].

xtupn.num1
1
xtupn[:texto1]
"texto"

El paquete Parameters, mediante la macro @unpack, brida funcionalidad para desempaquetar un conjunto de parámetros usando tuplas con nombre o identificador. Esto es muy útil para la definición de parámetros de un modelo. Por ejemplo, desempaquetemos sólo los elementos num1 y texto1 de la tupla xtupn:

using Parameters
@unpack num1, texto1 = xtupn
(num1 = 1, num2 = 2, num3 = 3, texto1 = "texto")

Adicionalmente, se puede usar la macro @with_kw para construir tuplas con parámetros por defecto y usar el nombre de la tupla para construir otra tupla cambiando uno o mas parámetros. Por ejemplo:

param = @with_kw= 0.4, β = 0.5, δ = 0.8)
@show p1 = param()       # Crea una tupla con parámetros por defecto
@show p2 = param(β = 1); # Crea una tupla con parámetros por defecto pero cambiando β.
p1 = param() = (α = 0.4, β = 0.5, δ = 0.8)
p2 = param(β = 1) = (α = 0.4, β = 1, δ = 0.8)

A continuación usamos @unpack para desempaquetar estos parámetros:

@unpack α, δ = p2;
@show α
@show δ;
α = 0.4
δ = 0.8
  1. Los Diccionarios se crean utilizando la siguiente sintaxis: Dict(clave1 => valor1, clave2 => valor2, ...)
notas = Dict("Javier" => 6.5, "Camila" => 6.8, "Ernesto" => "No Rindio el Examen")
Dict{String,Any} with 3 entries:
  "Ernesto" => "No Rindio el Examen"
  "Camila"  => 6.8
  "Javier"  => 6.5

Cada elemento puede ser recuperado usando su clave asociada bajo el sintaxis nombre_dic["clave"]:

notas["Camila"]
6.8

Los Diccionarios son mutable por lo que podemos cambiar su contenido:

notas["Ernesto"] = 7.0
7.0
notas
Dict{String,Any} with 3 entries:
  "Ernesto" => 7.0
  "Camila"  => 6.8
  "Javier"  => 6.5

Es posible además adicionar elementos definiendo una clave inexistente y su valor:

notas["Paola"] = 6.5
6.5
notas
Dict{String,Any} with 4 entries:
  "Ernesto" => 7.0
  "Paola"   => 6.5
  "Camila"  => 6.8
  "Javier"  => 6.5

Para eliminar una entrada del Diccionario usamos la función pop!(Diccionario, "clave")

pop!(notas, "Javier")
6.5
notas
Dict{String,Any} with 3 entries:
  "Ernesto" => 7.0
  "Paola"   => 6.5
  "Camila"  => 6.8
  1. Los Arreglos se crean utilizando el siguiente sintaxis: nombre = [item1, item2, ...]
lista_nombres = ["Javier", "Camila", "Ernesto", "Paola"]
4-element Array{String,1}:
 "Javier" 
 "Camila" 
 "Ernesto"
 "Paola"  
secuencia_numeros = [3.8, 4.0, 2.5, 6.8, 9.0]
5-element Array{Float64,1}:
 3.8
 4.0
 2.5
 6.8
 9.0
arreglo_mexclado = [3.8, 4.0, 2.5, 6.8, "texto"]
5-element Array{Any,1}:
 3.8     
 4.0     
 2.5     
 6.8     
  "texto"

Al igual que con las Tuplas, cada elemento puede ser recuperado usando el índice asociado a su ordenamiento (empezando en 1) bajo el sintaxis nombre_arreglo[indice]:

secuencia_numeros[4]
6.8

Los Arreglos son mutable por lo que podemos cambiar su contenido:

secuencia_numeros[4] = 4.0
4.0
secuencia_numeros
5-element Array{Float64,1}:
 3.8
 4.0
 2.5
 4.0
 9.0

Es posible editar el contenidos de un Arreglo usando las funciones push! y pop!.

  • push! adiciona un elemento al final del arreglo.
  • pop! remueve el último elemento del arreglo.
fibonacci = [1, 1, 2, 3, 5, 8, 13]
7-element Array{Int64,1}:
  1
  1
  2
  3
  5
  8
 13
push!(fibonacci, 21)
8-element Array{Int64,1}:
  1
  1
  2
  3
  5
  8
 13
 21
pop!(fibonacci)
21
fibonacci
7-element Array{Int64,1}:
  1
  1
  2
  3
  5
  8
 13

Como los arreglos (y también las Tuplas y los Diccionarios) almacenan información, es posible construir Arreglos que contengan otros Arreglos:

arreglos_compuestos = [[3, 4, 1], [2, 1], 3, [6, 7, 8, 9, 11]]
4-element Array{Any,1}:
  [3, 4, 1]       
  [2, 1]          
 3                
  [6, 7, 8, 9, 11]

Finalmente, se pueden crear Arreglos vacíos con el objetivo es reservar espacio en la memoria.

arrogle_vacio = []
0-element Array{Any,1}

Nota: Julia trata de optimizar el espacio usado en memoria no duplicando por defecto elementos. Así, si uno asigna un elemento a otro nombre usando = no está creando una copia sino una máscara para el mismo elemento. Esto es importante, porque si uno cambia el segundo elemento creado este cambio se reflejará también en el primer elemento. Por ejemplo:

x = [3, 4, 5]
3-element Array{Int64,1}:
 3
 4
 5
y = x
3-element Array{Int64,1}:
 3
 4
 5
y[3] = 100
100
x
3-element Array{Int64,1}:
   3
   4
 100

Para evitar esto se usa la función copy() (para estructuras más complejas se usa deepcopy(), copia además elementos dentro elementos como Arreglos dentro Arreglos).

z = copy(x)
3-element Array{Int64,1}:
   3
   4
 100
z[3] = 1000
1000
x
3-element Array{Int64,1}:
   3
   4
 100

3.8 Imprimir Resultados en Pantalla

Para mostrar el valor de las variables en pantalla usamos println() (imprime el contenido en pantalla y deja el cursor en la siguiente línea). Algunos ejemplos:

NombreLargo = 4.5
println(NombreLargo)
println(Y_1);
println("*** Otra forma ***") # Note que esta es una variable de texto definida directamente para 
println("---------")                              
4.5
Texto
*** Otra forma ***
---------

Es posible extrapolar valores dentro una variable de texto usando "$var". Por ejemplo:

x = 2
y = "$x es par"
"2 es par"

Esta idea se puede usar en combinación con la función println() para mostrar resultados en pantalla.

prof_name = "Mauricio Tejada"
prof_of = 211
prof_email = "matejada@uahurtado.cl"

println("Mi nombre es $prof_name") 
println("Mi oficina es la $prof_of y mi email es $prof_email")
Mi nombre es Mauricio Tejada
Mi oficina es la 211 y mi email es matejada@uahurtado.cl

3.9 Elementos Básicos para Graficar

using Plots

Ejemplo 1:

x = collect(0:pi/100:2*pi)
y = sin.(x)

plt_sin = plot(x,y, xlabel="x", ylabel="f(x)", title = "Función Seno", 
               color="blue", legend=false, linewidth = 2, grid = true)
display(plt_sin)

svg

Ejemplo 2:

x = range(-2*pi, stop = 2*pi, length = 50)
y1 = sin.(x)
y2 = cos.(x)

plt_sincos = plot(x,[y1 y2], xlabel="x", ylabel="f(x)", title = "Función Seno y Coseno", 
                  color=["blue" "red"], label=["sin(x)" "cos(x)"], legend = true, 
                  linewidth = 2, grid = true)
display(plt_sincos)

svg

Existen muchas opciones para personalizar un gráfico:

Estilos (usamos la opción line): - :solid Línea sólida (por defecto) - :dash Línea cortada. - :dot Línea punteada. - :dashdot Línea cortada-puntada.

Colores (usamos la opción color): - :yellow Amarillo. - :red Rojo. - :green Verde. - :blue Azul. - :white Blanco. - :black Negro.

Marcadores principales (usamos la opción shape): - :circle - :square - :diamond - :hexagon - :cross - :pentagon - :vline - :hline - :+ - :x

Para un listado completo de las opciones para personalizar gráficos ver el manual de Plots en http://docs.juliaplots.org/latest/

Ejemplo 3:

x = range(-2*pi, stop = 2*pi, length = 50)
y1 = sin.(x)
y2 = cos.(x)

plt_sincos = plot(x,[y1 y2], xlabel="x", ylabel="f(x)", title = "Función Seno y Coseno", 
                  color=[:blue :red], label=["sin(x)" "cos(x)"], legend = true, 
                  linewidth = 2, shape = [:circle :diamond], line = [:dot :dash],
                  grid = true)
display(plt_sincos)

svg

Usamos la función savefig para exportar un gráfico a un archivo, por ejemplo, pdf.

?savefig
search: [0m[1ms[22m[0m[1ma[22m[0m[1mv[22m[0m[1me[22m[0m[1mf[22m[0m[1mi[22m[0m[1mg[22m [0m[1mS[22mt[0m[1ma[22mckO[0m[1mv[22m[0m[1me[22mr[0m[1mf[22mlowError
savefig([plot,] filename)

Save a Plot (the current plot if plot is not passed) to file. The file type is inferred from the file extension. All backends support png and pdf file types, some also support svg, ps, eps, html and tex.

savefig(plt_sincos, "mytestplot.pdf")