Física Computacional - FSC-5705

só um divisor

Vetores e matrizes com numpy

Antes de iniciar devo advertir que este material é um resumo de uma biblioteca enorme, se você deseja informação detalhada a respeito dela clique aqui ou procure no google sobre numpy documentation. Exemplos do uso desta biblioteca podem ser encontrados em nullege.com

A principal diferencia com as listas é que as matrizes no numpy são de um tipo só ou seja, nas listas da para misturar string, inteiros, reais, complexos, etc, o que você bem entender; já nos vetores do numpy você só pode usar objetos de um mesmo tipo ou inteiros, ou reais ou complexos (sendo que cada um deles pode ser de diversas precisões, como se mostra na tabela embaixo)

Tipo Descrição
bool verdadeiro ou Falso, 1 bit
inti inteiro caraterístico do sistema (ou int32 ou int64)
int8 Byte ($-128$ a $127$)
int16 Inteiro ($-32768$ a $32767$)
int32 Inteiro $\left(2\times10^{31}\right.$ a $\left.2\times10^{31}-1\right)$
int64 Inteiro $\left(2\times10^{63}\right.$ a $\left.2\times10^{63}-1\right)$
uint8 Byte ($0$ a $255$)
uint16 Inteiro ($0$ a $65535$)
uint32 Inteiro $\left(0\right.$ a $\left.2\times10^{32}-1\right)$
uint64 Inteiro $\left(0\right.$ a $\left.2\times10^{64}-1\right)$
float16 real de precisão média: bit de sinal, expoente de 5 bits e 10 bits de mantisa
float32 real de precisão simples: bit de sinal, expoente de 8 bits e 23 bits de mantisa
float64 ou float real de precisão dupla: bit de sinal, expoente de 11 bits e 52 bits de mantisa
complex64 número complexo formado por 2 float32 - parte real e imaginaria
complex128 ou complex número complexo formado por 2 float64 - parte real e imaginaria

Operações com vetores do numpy

As tabelas desta seção são uma reprodução das encontradas na apresentação do professor Fabricio Ferrari.

Vejamos algumas das operações, elemento a elemento que podem ser feitas sobre vetores numpy. Se você vai usar algum desse métodos lembre-se que devemos escrever numpy.metodo onde método é alguma das operações que são descrita a seguir:

Definição dos elementos da matriz
Método Resultado
zeros((N,M)) vetor com zeros, M linhas, N colunas
ones((M,N)) vetor com uns, MxN
empty((M,N)) vetor vazio (qualquer valor), MxN
   
zeros_like(A) vetor com zeros, formato do A.
ones_like(A) vetor com uns, formato do A.
empty_like(A) vetor vazio, formato do A.
   
random.random((M,N)) vetor com números aleatórios, MxN
identity(N,float) matriz identidade NxN, ponto flutuante
     array([(1.5,2,3),(4,5,6)])      especifica os valores da matriz
mgrid[1:3,2:5] grade retangular x=[1,2] e y=[2,3,4]
   
fromfunction(f, (M,N)) matriz calculada com função f(i,j), MxN
arange(I, F, P) vetor com inicio I, fim F, passo P
linspace(I,F,N) vetor com N números de I até F
Operações sobre matrizes numpy
Método Resultado
A.sum() soma dos itens
A.min() valor mínimo
A.max() valor máximo
A.mean() média aritmética
A.std() desvio padrão
A.var() variância
A.trace() traço
A.size() número de elementos
A.shape() formato
A.ptp() pico-a-pico (máximo - mínimo)
A.ravel() versão 1D
A.transpose(), A.T matriz transposta
A.resize(M,N) reforma ou trunca a matriz in situ
A.reshape(M,N) retorna matriz com novo formato
A.clip(Amin,Amax) corta valores em Amin e Amax
     A.compress(condição)      seleciona elementos baseado em condição
A.conjugate() complexo conjugado
A.copy() retorna copia
A.fill(valor) preenche com valor

Método Resultado
C = A-B, C=A+B, C=A*B,
C=A/B, A**2
operações elemento a elemento,
$\left( C_{ij} = A_{ij} - B_{ij} \right)$
   
dot(A,B), mat(A)*mat(B) produto matricial
inner(A, B) produto interno
outer(A, B) produto externo
    concatenate(arrays, axis=0)     concatena vetores
vstack(A,B) empilha verticalmente vetores
hstack(A,B) empilha horizontalmente vetores
vsplit(A,2) parte verticalmente vetor
hsplit(A,2) parte horizontalmente vetor
   
A[0] primeiro elemento
A[i][j], A[i,j] convenção dos índices $A_{ij}$
(linha $i$, coluna $j$)
A[3][2] $A_{32}$ 3ro elemento na 4ta linha
A[1] 2da linha
   
x[2] 3ro elemento
x[-2] penúltimo elemento (índice contando do fim)
x[2:5] subvetor de 3ro até o quinto,
$\left[ x[2],\,x[3],\,x[4]\right]$
x[:5] elementos desde x[0] até x[4]
x[2:] do terceiro elemento até o fim
x[:] todo o vetor
x[2:9:3] do terceiro até o decimo elemento de,
três em três: $\left[ x[2],\,x[5],\,x[8]\right]$
x[numpy.where(x>7)] elementos em x maiores que 7

vejamos alguns exemplos, para definirmos uma matriz ou um vetor existem diversas forma:

        #!/usr/bin/env python

        import numpy as np

        print '\n---- De uma lista python para um vetor numpy -----'
        lista = []
        lista.append(1.0+1j)
        lista.append(2.0+5j)
        lista.append(3.0-3j)
        a = np.array(lista, dtype=np.complex)
        print lista, a, a.dtype, a.shape

        print '\n-------- Usando array --------'
        a = np.array([23, 5, 14])
        print a, a.dtype, a.shape

        a = np.array([True, False, True])
        print a, a.dtype, a.shape

        a = np.array([[1.0, 3.0, 5.0], [7.0, 9.0, 11.0], [13.0, 15.0, 17.0]], dtype=np.float32)
        print a, a.dtype, a.shape

        print '\n-------- Usando arange ----------'
        a = np.arange(5,dtype=np.float16)
        print a, a.dtype, a.shape

        a = np.arange(15,dtype=np.float16).reshape(3,5)
        print a, a.dtype, a.shape

        a = np.arange(1.0, 10.0, 2.0, dtype=np.float)
        print a, a.dtype, a.shape

        print '\n----------- Usando zeros, ones e empty --------------'
        a = np.zeros( 5, dtype=np.int32 )
        print a, a.dtype, a.shape

        a = np.ones( 5, dtype=np.complex )
        print a, a.dtype, a.shape

        a = np.empty( 16, dtype=np.float32 ).reshape(4,4)
        print a, a.dtype, a.shape

        print '\n------ Modificando um elemento da matriz -------'
        np.set_printoptions(precision=1)          
        a[3,3] = 1.0
        a[2,1] = 9.0
        print a
      
[usuario@python:exemplo ]# python exemplo24.py
[(1+1j), (2+5j), (3-3j)] [ 1.+1.j 2.+5.j 3.-3.j] complex128 (3,)

Usando Array
[23 5 14] int64 (3,)
[ True False True] bool (3,)
[[ 1. 3. 5.]
[ 7. 9. 11.]
[ 13. 15. 17.]] float32 (3, 3)

Usando Arange
[ 0. 1. 2. 3. 4.] float16 (5,)
[[ 0. 1. 2. 3. 4.]
[ 5. 6. 7. 8. 9.]
[ 10. 11. 12. 13. 14.]] float16 (3, 5)
[ 1. 3. 5. 7. 9.] float64 (5,)

Usando linspace: gera dados desde inicio ate fim,
igualmente espaciados, incluindo extremos
[ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
[ 0. 0.25 0.5 0.75 1. 1.25 1.5 1.75 2. 2.25
2.5 2.75 3. 3.25 3.5 3.75 4. 4.25 4.5 4.75 5.
5.25 5.5 5.75 6. 6.25 6.5 6.75 7. 7.25 7.5
7.75 8. 8.25 8.5 8.75 9. 9.25 9.5 9.75 10. ]
Usando Zeros
[0 0 0 0 0] int32 (5,)

Usando Ones
[ 1.+0.j 1.+0.j 1.+0.j 1.+0.j 1.+0.j] complex128 (5,)

Usando Empty
[[ -1.57829049e-07 4.58392754e-41 2.81180559e-37 0.00000000e+00]
[ 3.17444459e-37 0.00000000e+00 3.17448405e-37 0.00000000e+00]
[ 3.17452351e-37 0.00000000e+00 3.17456297e-37 0.00000000e+00]
[ 3.17460243e-37 0.00000000e+00 3.17464190e-37 0.00000000e+00]] float32 (4, 4)

modificando um elemento da matriz
[[ -1.6e-07 4.6e-41 2.8e-37 0.0e+00]
[ 3.2e-37 0.0e+00 3.2e-37 0.0e+00]
[ 3.2e-37 9.0e+00 3.2e-37 0.0e+00]
[ 3.2e-37 0.0e+00 3.2e-37 1.0e+00]]

agora vejamos operações sobre os vetores:

        #!/usr/bin/env python

        import numpy as np

        #De uma lista python para um vetor numpy
        print 'Converte de uma lista para array'
        lista = []
        lista.append(1.0+1j)
        lista.append(2.0+5j)
        lista.append(3.0-3j)
        a = np.array(lista, dtype=np.complex)
        print lista, a, a.dtype, a.shape

        #Usando array
        print '\nUsando Array'
        a = np.array([23, 5, 14])
        print a, a.dtype, a.shape

        a = np.array([True, False, True])
        print a, a.dtype, a.shape

        a = np.array([[1.0, 3.0, 5.0], [7.0, 9.0, 11.0], [13.0, 15.0, 17.0]], dtype=np.float32)
        print a, a.dtype, a.shape

        #Usando arange
        print '\nUsando Arange'
        a = np.arange(5,dtype=np.float16)
        print a, a.dtype, a.shape

        a = np.arange(15,dtype=np.float16).reshape(3,5)
        print a, a.dtype, a.shape

        a = np.arange(1.0, 10.0, 2.0, dtype=np.float)
        print a, a.dtype, a.shape

        #Usando linspace
        print '\nUsando linspace: gera dados desde inicio ate fim,'
        print 'igualmente espaciados, incluindo extremos'
        a = np.linspace(0,10,11)
        print a
        a = np.linspace(0,10,41)
        print a

        #usando zeros, ones e empty
        print '\nUsando Zeros'
        a = np.zeros( 5, dtype=np.int32 )
        print a, a.dtype, a.shape

        print '\nUsando Ones'
        a = np.ones( 5, dtype=np.complex )
        print a, a.dtype, a.shape

        print '\nUsando Empty'
        a = np.empty( 16, dtype=np.float32 ).reshape(4,4)
        print a, a.dtype, a.shape

        np.set_printoptions(precision=1)
        print '\nmodificando um elemento da matriz'
        a[3,3] = 1.0
        a[2,1] = 9.0
        print a
      

Lendo e escrevendo no disco

O numpy possui uma função para ler desde um arquivo uma tabela de dado, a função loadtxt() e seu uso é muito simples, como se mostra no exemplo embaixo (use os dados: dados.dat):

        #!/usr/bin/env python

        from numpy import *

        print '\nLendo todos os dados e armazenando em uma matriz'
        DataIn = loadtxt('dados1.dat')
        print DataIn

        print '\nLendo dado a dado da matriz'
        for line in DataIn:
          print line[0], line[1], line[2]


        print '\nUsando unpack'
        x, y, yerr = loadtxt('dados1.dat', unpack=True)

        for i in range(len(x)):
          print x[i], y[i], yerr[i]

        print '\nUsando unpack para ler colunas especificas'
        x, y = loadtxt('dados1.dat', unpack=True, usecols=[0,1])

        for i in range(len(x)):
          print x[i], y[i]
      

Observe que foi colocada a opção unpack=True para permitir que a matriz lida fosse separada em vetores que foram armazenados nas variáveis x, y, yerr. Além dessa opção existe varias outras como pode se ver no manual de referencia (em ingles) da função loadtxt() mas uma outra opção que devemos prestar atenção é a opção delimiter= essa opção espera uma string que diz para a função utilizar aquela string como separador, por exemplo delimiter=',', caso não seja definido um delimitador se utiliza o valor padrão que é o espaço em branco. Algumas vezes nossos dados tem algumas linhas de comentário, a função savetxt permite pular os comentarios sem dar erro se colocada a opção comments='#', note que da forma como colocado aqui diz que o símbolo utilizado para comentar é o '#', mas poderia ser outro. Um outro ponto forte desta função é que ela lê de forma transparente dados que estão "gzipados".

Quando queremos salvar uma matriz no disco o numpy possui a função savetxt(). As opções desta função são poucas:

numpy.savetxt(nome, Array, fmt='%.18e', delimiter=' ')

onde nome é o nome do arquivo que será criado como os dados e Array é a variável de tipo numpy.array que tem os dados. A formatação é do mesmo tipo que vimos anteriormente (clique aqui para ver formatação em detalhe) a qual também podemos utilizar com loadtxt, mas para ter uma ideia rápida a formatação segue a forma:

%[flags][largura][.precisão]código

Vejamos um exemplo:

      #!/usr/bin/env python
      #-*- coding: utf-8 -*-

      import numpy as np

      a = np.array([[1.0, 1.0, 0.05], [2.0, 4.0, 0.025], [3.0, 9.0, 0.32],\
      [4.0, 16, 0.03], [5.0, 25, 0.1]])

      print a

      np.savetxt('dados.dat', a)
      
[usuario@python:exemplo ]# python exemplo4.py
[[ 1. 1. 0.05 ]
[ 2. 4. 0.025]
[ 3. 9. 0.32 ]
[ 4. 16. 0.03 ]
[ 5. 25. 0.1 ]]
[usuario@python:exemplo ]# cat dados.dat
1.000000000000000000e+00 1.000000000000000000e+00 5.000000000000000278e-02
2.000000000000000000e+00 4.000000000000000000e+00 2.500000000000000139e-02
3.000000000000000000e+00 9.000000000000000000e+00 3.200000000000000067e-01
4.000000000000000000e+00 1.600000000000000000e+01 2.999999999999999889e-02
5.000000000000000000e+00 2.500000000000000000e+01 1.000000000000000056e-01

Observe que como não foi dada uma formatação ele imprime cada coluna com toda a precisão que possui por padrão. Se tivéssemos colocado como nome 'dados.dat.gz' a função savetxt() automaticamente gziparia os dados.

Números aleatórios

Por números aleatórios entendemos números que pertence a uma série de número os quais se caraterizam por não apresentam nenhum tipo de correlação entre eles, por exemplo, dado o $n-esimo$ elemento da série, os números prévios a ele não permitem determinar este de forma precisa. Obviamente esta definição é bem simples e minimalista já que a área da matemática que trata do problema é ampla.

No caso dos computadores a forma comum de gerar um número aleatórios é utilizando uma função matemática que gera um número pseudoaleatórios. Esses números são pseudoaleatórios porque a função (algoritmo) que gera eles são funções periódicas (não harmônicas), a função só consegue gerar $2^k-1$ números (onde é tipicamente $k=32$ ou $64$) totalmente aleatórios, não é infinita, assim se você gerar mais números do que o período ($2^k-1$) você gerará números que são os mesmo já previamente gerados (assim correlacionados).

Como os geradores de números aleatórios são funções (algoritmos) você tem que dar um valor chamado semente (seed) para ele devolver o número aleatório (tipo você tem que dar o $x$ para ter o $f(x)$, no caso dos geradores $x$ é um inteiro e $f(x)$ é um float), por sorte você sempre tem que dar o mesmo número durante o uso do gerador dentro do seu programa já que o gerador gera o número aleatório e uma nova semente, se você chama o gerador com a mesma semente inicial ele reconhece e utiliza aquela que tinha gerado.

O algoritmo utilizado no numpy para gerar números aleatórios é o algoritmo Mersenne Twister. Para demostrar seu uso o seguente programa vari gerar um conjunto de 20 números aleatórios distribuídos de forma uniforme:

      #!/usr/bin/env python
      #-*- coding: utf-8 -*-

      import numpy as np

      xi       = 0.0
      xf       = 1.0
      numDados = 2
      semente  = 938284

      #iniciando o gerador
      conjAleaNum = np.random.RandomState(seed=semente)

      #gerando os números
      print 'números gerados com a semente ', semente, ":"
      a = conjAleaNum.uniform(low=xi, high=xf, size=numDados)
      print a, "\n"

      print 'outros números gerados sem modificar a semente ', semente, ":"
      b = conjAleaNum.uniform(low=xi, high=xf, size=numDados)
      print b, "\n"

      print 'outros números gerados modificando a semente para ', semente, ":"
      conjAleaNum = np.random.RandomState(seed=semente)
      c = conjAleaNum.uniform(low=xi, high=xf, size=numDados)
      print c, "\n"

      print 'o dobro de números gerados modificando a semente para ', semente, ":"
      conjAleaNum = np.random.RandomState(seed=semente)
      d = conjAleaNum.uniform(low=0.0, high=xf, size=2*numDados)
      print d
      
[usuario@python:exemplo ]# python exemplo5.py
números gerados com a semente 938284 :
[ 0.9516993 0.43946049]

outros números gerados sem modificar a semente 938284 :
[ 0.59462894 0.37650768]

outros números gerados modificando a semente para 938284 :
[ 0.9516993 0.43946049]

o dobro de números gerados modificando a semente para 938284 :
[ 0.9516993 0.43946049 0.59462894 0.37650768]

Neste exemplo devemos prestar atenção nos seguintes detalhes, na linha 12 foi criado uma variável (objeto) chamado conjAleaNum (), essa variável é o conjunto conjunto dos números aleatórios que foi inicializado com a semente semente. Na linha 16 retiramos numDados elementos do conjunto (10 no exemplo) que estejam no intervalo $[low=xi, high=xf)$ - inclui $xi$ mas não $xf$ - e são armazenados no vetor a. Na linha 20 são retirados outros numDados elementos do conjunto e na linha 25 se repete o mesmo procedimento, contudo observe que na linha 24 se reinicio o conjunto de números aleatorios ao chamar a função np.random.RandomState(seed=semente). A prova disto é que no resultado o primeiro grupo de numDados é diferente do II grupo mas igual ao terceiro. Melhor ainda, para mostrar que conjAleaNum é uma secuencia contínua de dados, na linha 29 se reinicia a sequencia e se extra 2 vezes conjAleaNum e o conjunto resultante é a união dos I e II grupo que foram retirados.

Tipo de distribuições

No exemplo acima vemos que os números são retirados de forma a estarem uniformemente distribuídos, o que significa isso? Para entender teremos que fazer um gráfico de N=100 valores sorteados os quais serão colhidos de uma distribuição uniforme entre -1 e 1.

      #!/usr/bin/env python
      #-*- coding: utf-8 -*-

      import numpy as np
      import matplotlib as mpl
      import matplotlib.pyplot as plt

      xi       = -1.0
      xf       = 1.0
      numDados = 1000
      semente  = 4727

      #iniciando o gerador
      conjAleaNum = np.random.RandomState(seed=semente)

      #gera os número
      y = conjAleaNum.uniform(low=xi, high=xf, size=numDados)

      #cria numDados entre 0 e numDados igualmente espaçados
      x = np.linspace(1, numDados, numDados)

      #cria o gráfico
      plt.plot(x, y, '-')
      plt.show()
      

O resultado desse script é

Ou seja, o programa gerou 1000 pontos entre -1 e 1, a pergunta agora é qual é a probabilidade de encotrar um número entre 0.1 e 0.2? Para responder isso devemos dividir o intervalo $[-1,1)$ em pequenos intevalinhos de tamanho 0.1 e contar quantos números caim nesse no intervalo que queremos (no caso entre 0.1 e 0.2), afortunadamente isto é relativamente simples utilizando matplotlib, basta utilizar a função hist e pedir que seja feito 20 intervalos ($(1.0 - (-1.0))/0.1 = 20$):

      #!/usr/bin/env python
      #-*- coding: utf-8 -*-

      import numpy as np
      import matplotlib as mpl
      import matplotlib.pyplot as plt

      xi       = -1.0
      xf       = 1.0
      numDados = 1000
      semente  = 4727

      #iniciando o gerador
      conjAleaNum = np.random.RandomState(seed=semente)

      #gera os número
      y = conjAleaNum.uniform(low=xi, high=xf, size=numDados)

      #cria numDados entre 0 e numDados igualmente espaçados
      x = np.linspace(1, numDados, numDados)

      plt.hist(y, bins=20)
      plt.show()
      

O que resulta em:

Da figura vemos que algo perto de 40 dos 1000 números estão entre 0.1 e 0.2, mas note que quase todos estão dentro dessa faixa, de fato, se nos aumentamos para $1\times10^6$ a quantidade de número teremos as figura abaixo

De onde vemos que os números se distribuem uniformemente no intervalo isto é, temos a mesma probabilidade de encontrar um número em qualquer seção do intervalo.

A outra distribuição muito utilizada em física é a distribuição gaussiana ou normal que tem a forma funcional: \[ p(x) = \frac{1}{\sqrt{ 2 \pi \sigma^2 }} e^{ - \frac{ (x - \mu)^2 } {2 \sigma^2} } \nonumber \] onde $\sigma$ é a largura da gaussiana e $\mu$ onde ela está centrada. O script a seguir mostra como implementar com numpy a geração de pontos que respeitem o comportamento gaussiano:

      #!/usr/bin/env python
      #-*- coding: utf-8 -*-

      import numpy as np
      import matplotlib as mpl
      import matplotlib.pyplot as plt

      centro   = 0.0
      largura  = 1.0
      numDados = 1000
      semente  = 4727

      #iniciando o gerador
      conjAleaNum = np.random.RandomState(seed=semente)

      #gera os número
      y = conjAleaNum.normal(loc=centro, scale=largura, size=numDados)

      #cria numDados entre 0 e numDados igualmente espaçados
      x = np.linspace(1, numDados, numDados)

      plt.hist(y, bins=20)
      plt.show()
      

Aumentando a quantidade de dados a ser gerada para um milhão e colocando o bins=100 o resultado é

Além dessas duas distribuições a numpy define varias outras, como a poisson pode ser visto no site do numpy seção de números aleatórios.

Tarefa

  1. Escreva um programa que realize o produto de duas matrizes: $C = AB$. O programa deve perguntar para o usuário o número de linhas e colunas de cada matriz. Deve checar, utilizando o dado anterior, se o produto pode ser realizado efetivamente. Deve ler do teclado cada uma das matrizes e realizar seu produto segundo a equação $c_{ij} = \sum_{k=1}^{q} a_{ik}b_{kj}$. Como exemplo teste seu programa utilizando
    $\displaystyle{ \left[\begin{array}{c c c} 1 & 2 & 4 \\ 2 & 6 & 0 \end{array}\right] \; \left[\begin{array}{c c c c} 4 & 1 & 4 & 3 \\ 0 & -1 & 3 & 1 \\ 2 & 7 & 5 & 2 \end{array}\right] = \left[\begin{array}{c c c c} 12 & 27 & 30 & 13 \\ 8 & -4 & 26 & 12 \end{array}\right] }$
  2. Existe um teorema matemática que permite calcular as novas coordenadas de um vetor após ter sido realizada uma rotação em torno de um vetor unitário $\vec u$. Esse teorema diz que: Se $\vec u = a\hat i+b\hat j+c\hat k$ é um vetor unitário, então a matriz canônica $R_{\vec u , \theta}$ da rotação pelo ângulo $\theta$ em torno do eixo pela origem com orientação $\vec u$ é dada por
    $\displaystyle{ R_{\vec u , \theta} = \left[\begin{array}{c c c} a^2\left(1-\cos\theta\right)+\cos\theta & ab\left(1-\cos\theta\right)-c\sin\theta & ac\left(1-\cos\theta\right)+b\sin\theta \\ ab\left(1-\cos\theta\right)+c\sin\theta & b^2\left(1-\cos\theta\right)+\cos\theta & bc \left(1-\cos\theta\right)-a\sin\theta \\ ac\left(1-\cos\theta\right)-b\sin\theta & bc \left(1-\cos\theta\right)+a\sin\theta & c^2\left(1-\cos\theta\right)+\cos\theta \end{array}\right] }$
    Dessa forma dado um vetor $\vec r$ o novo vetor que se obtêm apôs a rotação de $\theta$ em torno de $\vec u$ é o vetor $\vec r'$ dado por: $\vec r' = R_{\vec u , \theta} \vec r$. De posse desse teorema, construa um programa que:
    1. Pergunte a coordenada de um ponto, no espaço $(x,y,z)$, que permita traçar uma reta desde a origem, suporemos que o vetor unitário está sobre essa reta
    2. Pergunte as coordenadas do vetor que quer ser rotado
    3. Pergunte o ângulo de rotação, em graus (a matriz de rotação é em radianos)
    4. Como resultado o programa deve devolver o novo vetor resultante da rotação
    Como teste considere o ponto de referencia para o vetor unitário $(1,1,1)$, uma rotação de $120^\circ$ e o vetor a ser rotado é $\vec r = \hat i - \hat j$. O resultado dessa operação é o novo vetor $\vec r' = \hat j - \hat k$.