Rutas
/
Julia
Julia
/
Ejercicios
/
Revolución robótica
Revolución robótica

Revolución robótica

Ejercicio de aprendizaje

Introducción

¿Qué es el «álgebra lineal»?

Hay muchas definiciones técnicas, en [Wikipedia][linalg-wiki], en libros de texto y en sitios web excelentes como [3Blue1Brown][3blue1brown], pero para nuestros propósitos podemos ser más informales.

Note

El álgebra lineal trata de muchas cosas interesantes (y muy útiles) que puedes hacer con vectores y matrices.

A los mantenedores de Julia (y Python) les encanta este tipo de cosas. A la dirección de Exercism, no tanto.

Este concepto se limitará a los aspectos más sencillos de un tema enorme, pero te avisamos: esto es, inevitablemente, un concepto bastante matemático.

¿Qué es un vector?

Depende de a quién preguntes y de cómo quieras visualizar algo bastante abstracto.

  • En Julia, Vector{T} no es más que un alias de Array{T, 1}, y el eltype T puede ser cualquier cosa.
  • En (gran parte de) la física, un vector es una flecha con length y direction en un espacio de N dimensiones, pero sin una posición fija.
  • En álgebra lineal, la flecha de los físicos está fija en el espacio, con su cola en el origen. Esto hace que el cuerpo de la flecha sea redundante, así que podemos representar el vector como un point en un espacio de N dimensiones, con los N elementos que representan la distancia desde el origen a lo largo de cada uno de los N ejes.

Para lo que nos ocupa, ignora los vectores de strings o caracteres. En este concepto trabajaremos con tipos numéricos: Int, Float o Complex.

¿Qué es una matriz?

De nuevo, encontrarás una variedad de respuestas.

  • Un array rectangular bidimensional de números (sin bordes irregulares). Una matriz cuadrada es un caso especial habitual.
  • Una linear combination de vectores columna, apilados uno al lado del otro.
  • Una transformation que se puede aplicar a un vector, igual que una function se aplica a otros tipos de entrada.

Algunos tipos de matriz cuadrada son lo bastante comunes como para tener nombres especiales.

Matriz diagonal: Todas las entradas no nulas están en la main diagonal (de arriba a la izquierda a abajo a la derecha).

julia> [1 0 0; 0 2 0; 0 0 3]
3×3 Matrix{Int64}:
 1  0  0
 0  2  0
 0  0  3

# a convenient shortcut
julia> diagm(1:3)
3×3 Matrix{Int64}:
 1  0  0
 0  2  0
 0  0  3

Matriz identidad: Una matriz diagonal con solo unos en la diagonal (por razones que quedarán más claras más adelante). A menudo se abrevia como I.

julia> Matrix{Float64}(I, 3, 3)
3×3 Matrix{Float64}:
 1.0  0.0  0.0
 0.0  1.0  0.0
 0.0  0.0  1.0

Triangular superior: Valores no nulos en la diagonal y por encima de ella, ceros por debajo. Triangular inferior, puedes imaginártelo.

Transponer una matriz

Si de verdad quieres intercambiar filas por columnas, la función permutedims() hará esto y te dará una nueva matriz.

Esto implica copiar, lo cual es lento y consume mucha memoria cuando se trabaja con matrices grandes.

Para el álgebra lineal, la función transpose() es más útil, ya que crea rápidamente un envoltorio perezoso alrededor de la matriz original.

Aún más útil es la función adjoint(), que además invierte el signo de la parte imaginaria en cualquier número complejo. Esta operación es lo bastante común como para que baste con añadir un apóstrofo ' al nombre de la variable para crear la adjunta.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

# new, full copy
julia> permutedims(m)
3×2 Matrix{Int64}:
 1  4
 2  5
 3  6

# lazy version
julia> transpose(m)
3×2 transpose(::Matrix{Int64}) with eltype Int64:
 1  4
 2  5
 3  6

julia> mc = [1+2im 2+3im; 3+2im 1+2im]
2×2 Matrix{Complex{Int64}}:
 1+2im  2+3im
 3+2im  1+2im

# lazy conjugate transpose
julia> adjoint(mc)
2×2 adjoint(::Matrix{Complex{Int64}}) with eltype Complex{Int64}:
 1-2im  3-2im
 2-3im  1-2im

# syntactic sugar for adjoint
julia> mc'
2×2 adjoint(::Matrix{Complex{Int64}}) with eltype Complex{Int64}:
 1-2im  3-2im
 2-3im  1-2im

Multiplicación

El resto de este documento incluye funcionalidad del módulo LinearAlgebra. Una línea using LinearAlgebra lo traerá al espacio de nombres, pero no vamos a repetirlo en los ejemplos (demasiado desorden visual).

Vectores, producto elemento a elemento

Esto se trató en el concepto [Operaciones con vectores][vector-ops]. El operador es .*, que opera sobre los vectores de entrada por pares, dando una salida del mismo tamaño y tipo que las entradas.

julia> [1, 2] .* [3, 4]
2-element Vector{Int64}:
 3
 8

Vectores, producto escalar

Esta operación tan común se escribe en los libros de texto como u ⋅ v y se llama producto escalar.

Un producto escalar equivale a la suma del producto elemento a elemento.

Dos vectores se pueden multiplicar con el operador habitual *, pero solo si el vector izquierdo es una adjoint: convenientemente escrito u' * v. Esto quedará más claro en la sección posterior sobre multiplicación de matrices.

El punto elevado (centrado) está disponible en Julia (se introduce escribiendo \cdot y pulsando tabulador) como azúcar sintáctico para la función dot(). No es necesario especificar el adjunto, ya que este detalle se gestiona automáticamente.

julia> using LinearAlgebra

# with element-wise syntax
julia> sum([1, 2] .* [3, 4])
11

# with u' * v syntax
julia> [1, 2]' * [3, 4]
11

# with dot()
julia> dot([1, 2], [3, 4])
11

# with \cdot syntax
julia> [1, 2] ⋅ [3, 4]
11

Para vectores con valores complejos, el vector izquierdo debe ser el conjugado (con el signo de la parte imaginaria invertido). La función dot lo hace automáticamente, la sintaxis u' lo hace explícitamente, pero sum(u .* v) fallará si u y v son complejos.

Normas

¿Cómo de «grande» es un vector?

La norm es un intento de capturar esto, reduciendo el vector a un escalar adecuado.

Se define toda una familia de normas, pero de lejos la más común es la norma 2, que es √(v ⋅ v).

Esta operación de raíz cuadrada media es la distancia pitagórica desde el origen (en un espacio de N dimensiones). Si visualizamos el vector como una flecha, con su cola en el origen, la norma 2 es la longitud de la flecha.

Cualquier norma p se puede calcular proporcionando p como segundo argumento. La norma 1 a veces es útil: es simplemente la suma de los valores absolutos de las entradas (así que es muy rápida y fácil de calcular).

# defaults to the 2-norm
julia> norm([1, 2, 3])
3.7416573867739413
# the 1-norm
julia> norm([1, -2, 3], 1)
6.0

Multiplicación de matrices

A veces parece que todos los matemáticos aplicados del mundo han pasado gran parte de los últimos 80 años convirtiendo cada cálculo en una serie de multiplicaciones de matrices.

Esta es una operación en la que los ordenadores son muy buenos:

  • Es muy repetitiva.
  • Se puede paralelizar de forma eficiente.
  • Se ha desarrollado mucho hardware especializado para hacerla más rápida, incluida la GPU que probablemente esté integrada en tu portátil.

Los detalles son bastante sencillos, aunque no muy intuitivos a primera vista.

Considera una matriz A que multiplica un vector v para dar una salida w (por convención del álgebra lineal, usaremos letras mayúsculas para las matrices y minúsculas para los vectores).

La primera fila de A se multiplica escalarmente con v para obtener el primer elemento de w, la segunda fila da el segundo elemento, y así sucesivamente hacia abajo.

julia> A = [1 2; 3 4]
2×2 Matrix{Int64}:
 1  2
 3  4
julia> v = [5, 6]
2-element Vector{Int64}:
 5
 6
julia> A * v
2-element Vector{Int64}:
 17  # equals [1, 2] ⋅ [5, 6]
 39  # equals [3, 4] ⋅ [5, 6]

La multiplicación matriz * matriz extiende esto a lo largo de las columnas de la matriz de la derecha.

Para C = A * B, podemos considerar que A multiplica cada columna de B para dar la columna correspondiente de C: una serie de multiplicaciones matriz-vector.

De forma equivalente, podríamos decir que se hace el producto escalar de la primera fila de A con cada columna de B para dar la primera fila de C, la segunda fila da la segunda fila, y así sucesivamente hacia abajo.

julia> A
2×2 Matrix{Int64}:
 1  2
 3  4

julia> B = [5 7; 6 8]
2×2 Matrix{Int64}:
 5  7
 6  8

# left column is the same as A*v previously
julia> A * B
2×2 Matrix{Int64}:
 17  23
 39  53
 
julia> B * A
2×2 Matrix{Int64}:
 26  38
 30  44

Como se muestra en el ejemplo anterior, la multiplicación de matrices no conmuta: no hay una relación simple entre A*B y B*A.

Probablemente sea difícil de visualizar para quien se acerca por primera vez, solo con leer las palabras. YouTube tiene muchos vídeos que lo demuestran gráficamente, así que busca «multiplicación de matrices» y elige uno con tu estilo, nivel de detalle e idioma preferidos.

Dimensiones

El producto escalar de dos vectores depende de que tengan la misma longitud.

Por extensión, para la multiplicación de matrices, el número de columnas de la matriz izquierda debe coincidir con el número de filas de la matriz derecha.

Si expresamos los tamaños como tuplas (nrows, ncols), tal como las devuelve size(A), tenemos (a, b) * (b, c) -> (a, c). Las dimensiones «internas», aquí b y b, son compatibles para calcular productos escalares. Las dimensiones «externas», aquí a y c, determinan las dimensiones de la salida.

Un ejemplo con matrices rectangulares:

julia> D = reshape(1:6, 2, 3)
2×3 reshape(::UnitRange{Int64}, 2, 3) with eltype Int64:
 1  3  5
 2  4  6

julia> E = reshape(1:12, 3, 4)
3×4 reshape(::UnitRange{Int64}, 3, 4) with eltype Int64:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> D * E
2×4 Matrix{Int64}:
 22  49   76  103
 28  64  100  136

# (3, 4) * (2, 3) not possible
julia> E * D
ERROR: DimensionMismatch: matrix A has axes (Base.OneTo(3),Base.OneTo(4)), matrix B has axes (Base.OneTo(2),Base.OneTo(3))

Multiplicar pares de vectores al estilo de matrices tiene dos posibilidades.

Convencionalmente, usaríamos u' * v como equivalente al producto escalar. Las dimensiones son (1, 3) * (3, 1), y Julia simplifica la salida (1, 1) a un escalar (a diferencia, por ejemplo, de R).

Alternativamente, podríamos usar u * v', con dimensiones (3, 1) * (1, 3) -> (3, 3), para multiplicar todos los pares posibles de elementos y expandir los vectores a una matriz.

julia> u = [1, 2]
2-element Vector{Int64}:
 1
 2
julia> v = [3, 4]
2-element Vector{Int64}:
 3
 4
julia> u' * v
11
julia> u * v'
2×2 Matrix{Int64}:
 3  4
 6  8

u' * v a veces se llama producto interior.

Rotaciones

Un tipo de multiplicación de matrices particularmente común implica matrices de rotación.

En 2D, hay una matriz relativamente sencilla para rotar un vector en sentido antihorario θ radianes.

julia> rot2d(θ, vec) = [cos(θ) -sin(θ); sin(θ) cos(θ)] * vec
rot2d (generic function with 1 method)

# unit vector in the x direction
julia> i_hat = [1, 0]
2-element Vector{Int64}:
 1
 0

# rotate 45 degrees
julia> rot2d(π/4, i_hat)
2-element Vector{Float64}:
 0.7071067811865476
 0.7071067811865475

# rotate 90 degrees -> unit vector in the y direction, j_hat
julia> rot2d(π/2, i_hat)
2-element Vector{Float64}:
 6.123233995736766e-17  # zero, within numerical error
 1.0

Instrucciones

Trabajas para una startup de robótica que está desarrollando un robot sencillo como prueba de concepto. Se te ha encomendado la tarea de proporcionar cierta funcionalidad para controlar el movimiento del robot.

1. Orienta el robot

Para llevar un registro de la orientación y la extensión del robot, este tiene tres marcadores que están a 1 unidad de distancia de su centro. Para inicializar su posición, tenemos que tomar los tres vectores de dirección, normalizarlos y colocarlos en una matriz.

Implementa la función orientrobot(vectors), que recibe un vector de tres vectores. Devuelve una matriz 2x3 con los vectores normalizados como columnas.

julia> orientrobot([[-1,1],[1,0],[-1,-1]])
2×3 Matrix{Float64}:
 -0.707107  1.0  -0.707107
  0.707107  0.0  -0.707107

2. Rota el robot

A continuación, necesitamos funcionalidad para cambiar la dirección del movimiento. Para ello, tenemos que girar el robot para que mire hacia donde quiere ir.

Implementa la función rotaterobot(orientation, θ), que recibe la matriz de orientación del robot y un ángulo θ para girar en sentido antihorario. Devuelve la nueva matriz de orientación.

julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);

julia> rotaterobot(orientmatrix, π/2)
2×3 Matrix{Float64}:
 -0.707107  6.12323e-17   0.707107
 -0.707107  1.0          -0.707107

3. Comprueba que la orientación es correcta

Para mover el robot de un lugar a otro, primero tenemos que comprobar que tiene la orientación correcta antes de moverlo. La segunda columna de la matriz de orientación representa la dirección hacia delante.

Implementa la función robotoriented(orientation, direction), que recibe una matriz de orientación y un vector de posición relativa. Devuelve true si el robot está orientado en la misma dirección que el vector de posición relativa (dentro del error de redondeo).

julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);

julia> robotoriented(orientmatrix, [5, 0])
true

julia> robotoriented(orientmatrix, [0, 5])
false

julia> robotoriented(orientmatrix, [-5, 0])
false

4. Coordenadas del cuerpo del robot

Dado que la matriz de orientación también lleva un registro de la forma del robot, necesitamos saber dónde están estos puntos con respecto al origen después de mover el robot. Esto ayudará al robot a evitar colisiones con otros objetos cuando se mueve.

Implementa la función bodylocation(orientation, position), que recibe una matriz de orientación y la posición actual del centro del robot. Devuelve la matriz de orientación trasladada.

julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);

julia> bodylocation(orientmatrix, [5, 3])
2×3 Matrix{Float64}:
 4.29289  6.0  4.29289
 3.70711  3.0  2.29289
Editar en GitHub El enlace se abre en una ventana o pestaña nueva
Julia Exercism

¿Listo para empezar Revolución robótica?

Regístrate en Exercism para aprender y dominar Julia con 35 conceptos128 ejercicios y mentoría humana real, todo gratis.