Python para Sistemas Lineares
Antes de programar os métodos de Gauss, Gauss-Jordan, Jacobi e Gauss-Seidel, é importante conectar a escrita matemática de um sistema linear com as operações matriciais feitas em Python. Como listas, funções, laços e NumPy já foram apresentados na introdução ao Python, aqui o foco será representar sistemas, manipular linhas, controlar pivôs, calcular resíduos e organizar iterações.
Representando um sistema linear
Um sistema linear pode ser escrito na forma matricial:
\[Ax=b\]Nos algoritmos diretos, trabalharemos bastante com a matriz aumentada \([A|b]\). Em Python, uma forma simples de armazenar essa matriz é usar uma lista de listas:
M = [
[1, -2, -1, 2, 1],
[-2, 5, 3, -3, 4],
[-2, 6, 5, -2, 13],
[2, -4, -1, 5, 7]
]
Cada lista interna representa uma linha da matriz. A última coluna representa o vetor \(b\).
| Elemento matemático | Representação em Python | Uso no material |
|---|---|---|
| \(A\) | A |
Matriz dos coeficientes |
| \(b\) | b |
Vetor de termos independentes |
| \([A|b]\) | M |
Matriz aumentada usada nos métodos diretos |
| \(x\) | x |
Vetor solução ou chute inicial dos métodos iterativos |
Usando NumPy com tipo real
Para calcular com matrizes de forma mais direta, usaremos a biblioteca numpy. Ela permite multiplicar linhas por constantes, subtrair linhas, calcular produtos matriciais e medir erros com menos código. O uso de dtype=float é importante porque os métodos envolvem divisões e produzem valores reais.
import numpy as np
M = np.array([
[1, -2, -1, 2, 1],
[-2, 5, 3, -3, 4],
[-2, 6, 5, -2, 13],
[2, -4, -1, 5, 7]
], dtype=float)
Quando for necessário preservar a matriz original para conferência, use uma cópia antes de aplicar eliminações:
M_original = M.copy()
M_trabalho = M.copy()
Índices de linhas e colunas
Na matemática, é comum numerar linhas e colunas começando em 1. Em Python, os índices começam em 0. Assim, a primeira linha é M[0], a segunda linha é M[1] e assim por diante.
| Matemática | Python | Significado |
|---|---|---|
| \(L_1\) | M[0] |
Primeira linha |
| \(L_2\) | M[1] |
Segunda linha |
| \(M_{21}\) | M[1, 0] |
Elemento da segunda linha e primeira coluna |
| \(M_{ii}\) | M[i, i] |
Elemento da diagonal principal |
Operações com linhas
Nos métodos de Gauss e Gauss-Jordan, a operação mais repetida é substituir uma linha por uma combinação dela com outra linha:
\[L_j = L_j - \lambda L_i\]Em Python com NumPy, essa operação pode ser escrita de forma parecida com a expressão matemática:
fator = M[j, i] / M[i, i]
M[j] = M[j] - fator * M[i]
Como essa operação será repetida várias vezes, podemos criar uma função:
def eliminar(M, i, j):
fator = M[j, i] / M[i, i]
M[j] = M[j] - fator * M[i]
Nessa função, i é a linha pivô usada como referência para zerar um elemento, enquanto j é a linha alvo que será alterada. A expressão M[j] seleciona a linha inteira, permitindo atualizar todos os elementos da linha de uma vez.
Percorrendo pivôs e linhas alvo
Na eliminação de Gauss, cada coluna define uma linha pivô. Depois de escolher o pivô, percorremos apenas as linhas abaixo dele para zerar os elementos daquela coluna:
dimensao = 4
for i in range(dimensao - 1):
for j in range(i + 1, dimensao):
eliminar(M, i, j)
O primeiro laço escolhe a linha pivô. O segundo laço percorre as linhas abaixo dela, que receberão a operação de eliminação.
Pivô e troca de linhas
Antes de dividir pelo pivô, é preciso verificar se ele é nulo ou muito próximo de zero. Uma estratégia comum é trocar a linha atual pela linha abaixo que possui o maior valor absoluto naquela coluna:
coluna = i
linha_maior = i + np.argmax(abs(M[i:, coluna]))
if linha_maior != i:
M[[i, linha_maior]] = M[[linha_maior, i]]
A expressão M[linha_pivo:, coluna] seleciona a parte da coluna abaixo da linha pivô. Essa técnica será útil para tornar a eliminação mais estável e evitar divisões inválidas.
| Comando | Função no algoritmo |
|---|---|
range(n) |
Gera os valores de 0 até \(n-1\) |
range(a, b) |
Gera os valores de \(a\) até \(b-1\) |
for |
Repete um bloco de comandos para cada valor da sequência |
while |
Repete enquanto uma condição for verdadeira, útil nos métodos iterativos |
Separando \(A\), \(b\) e \(x\)
Nos métodos iterativos, como Jacobi e Gauss-Seidel, trabalharemos separando a matriz de coeficientes \(A\), o vetor \(b\) e o vetor de aproximações \(x\):
A = np.array([
[8, -2, 5],
[1, -5, 3],
[-1, 3, -10]
], dtype=float)
b = np.array([43, 3, -28], dtype=float)
x = np.array([0, 0, 0], dtype=float)
Essa separação permite escrever as contas de forma próxima da notação matricial. Por exemplo, o produto \(Ax\) é calculado com A @ x, e o resíduo do sistema pode ser obtido por b - A @ x.
Para calcular uma nova aproximação de uma incógnita, isolamos uma variável por vez. Em Python, o somatório dos outros termos pode ser feito com sum:
n = len(b)
i = 0
soma = sum(A[i, j] * x[j] for j in range(n) if j != i)
x_novo = (b[i] - soma) / A[i, i]
A condição if j != i remove o termo da diagonal do somatório, pois ele fica isolado no denominador. Esse padrão aparece tanto no método de Jacobi quanto no método de Gauss-Seidel.
Cálculo do erro
Nos métodos iterativos, precisamos verificar se a aproximação já está boa o suficiente. Uma forma de fazer isso é calcular o erro usando a diferença entre \(b\) e \(Ax\):
\[\xi = |b - Ax|\]Em Python com NumPy, essa conta pode ser escrita assim:
residuo = b - A @ x
erro_por_equacao = abs(residuo)
maior_erro = max(erro_por_equacao)
Outra forma comum é usar uma norma vetorial. Com NumPy, a norma infinito mede o maior erro absoluto entre as equações:
erro = np.linalg.norm(residuo, ord=np.inf)
Registrando aproximações iterativas
Nos métodos iterativos, é importante guardar a aproximação antiga e a nova para comparar a evolução. Use copy para evitar que as duas variáveis apontem para o mesmo vetor:
x_anterior = x.copy()
x_novo = x.copy()
# depois de calcular a nova iteração
erro_iteracao = np.linalg.norm(x_novo - x_anterior, ord=np.inf)
Exibindo resultados
Para acompanhar os cálculos, usaremos print. Quando for necessário misturar texto e valores, usaremos f-strings:
iteracao = 1
maior_erro = 0.025
print(f"Iteração {iteracao}: erro = {maior_erro}")
Com esses comandos, já é possível acompanhar as implementações das próximas aulas de sistemas lineares.
Atividade
Observe o código abaixo e calcule o resíduo b - A @ x para o chute inicial. Depois, use np.linalg.norm para medir o maior erro absoluto entre as equações:
import numpy as np
A = np.array([
[4, 1, -1],
[2, 7, 1],
[1, -3, 12]
], dtype=float)
b = np.array([3, 19, 31], dtype=float)
x = np.array([0, 0, 0], dtype=float)