Números complexos com Python e SymPy

números complexos python sympy

Muito provavelmente seu primeiro contato com números complexos foi no ensino médio de uma forma muito rápida e abstrata. No entanto, é um tópico fascinante e com muitas aplicações práticas passando por gráficos vetoriais, análise de frequência sonora até o estudo de fractais. Neste artigo, veremos como a linguagem Python lida com números complexos nativamente e como o pacote SymPy amplia o poder da linguagem.

Nem de longe é a pretensão deste artigo fazer uma grande abordagem da parte teórica de números complexos. Mas, caso o leitor precise de uma pequena ajuda por não ver o assunto há muito tempo, segue uma breve revisão.

Um número complexo é um elemento de um sistema numérico que contém números reais e um elemento denotado i, chamado de unidade imaginária, e que satisfaz a equação i^2 = -1. Em engenharia e computação, é comum usarmos a letra j para a unidade imaginária. Inclusive, esta é a forma utilizada nativamente pela linguagem Python.

Números complexos permitem que todas as equações polinomiais tenham soluções. Provavelmente, foi esse o ponto de partida do estudo de complexos de muitos que tiveram este tópico no ensino médio. Por exemplo, antes equações como (x + 1)^2 + 9 = 0 eram consideradas sem solução. No entanto, não possuem solução real, tendo em vista que usualmente são representadas num plano real. Vamos utilizar um pouco do que já vimos em outro artigo para fazer o gráfico desta equação no plano real:

from sympy import Symbol, solve, init_printing
from sympy.plotting import plot

# configuração para outputs melhores no artigo, pode ser ignorado
init_printing(use_latex='png', scale=1.25, order='grlex',
              forecolor='Black', backcolor='White', fontsize=12)

x = Symbol('x')
example = (x + 1)**2 + 9

plot(example, axis_center=(0, 0))
<sympy.plotting.plot.Plot at 0x7f50b6777d00>

Você aprendeu que o gráfico acima mostra que não há raízes reais para a equação em questão, já que não há interseção com o eixo x. Mas, tal equação possui soluções complexas: -1 +3i e -1 -3i. Podemos verificar com o próprio SymPy:

solve(example)

Certamente há muito mais o que falar sobre complexos, mas prefiro ir mostrando aos poucos. Antes de continuar explorando o assunto com o SymPy, vejamos como a linguagem Python lida com complexos nativamente. No caminho, vamos continuar revisando os principais aspectos de números complexos.

Números complexos em Python

Como já dito, a linguagem utiliza a letra j na representação de complexos:

z = 4 + 3j
type(z)
complex

É útil separar um número complexo em duas partes: a real e a imaginária, esta sendo o número real que multiplica a unidade imaginária. Podemos obter essas partes como atributos do nosso objeto z:

z.real
z.imag

Outro conceito útil no estudo de complexos é o de complexo conjugado. O conjugado de um dado complexo possui a mesma parte real, mas o sinal da parte imaginária é trocado. O conjugado pode ser obtido pelo método conjugate:

z.conjugate()
(4-3j)

Representações gráficas de números complexos

A primeira representação que costuma ser apresentada no estudo de complexos é a do plano de Argand-Gauss. Neste plano, o eixo horizontal representa a parte real do número complexo e o vertical a parte imaginária. Assim, dado um complexo na forma algébrica z = a + bi, obtemos um ponto (a, b) neste plano.

Agora, se temos um ponto em um plano podemos definir um vetor partindo da origem do sistema de coordenadas até o ponto. Vejamos, com a ajuda do Matplotlib:

import matplotlib.pyplot as plt

# para padronizar o estilo dos gráficos do artigo:
params = {
    'axes.labelsize': 12,
    'axes.titlesize': 12,
    'axes.spines.top': False,
    'axes.spines.right': False,
    'xtick.labelsize': 12,
    'ytick.labelsize': 12,
    'figure.autolayout': True,
    'figure.facecolor': 'white',
    'figure.titlesize': 16,    
    'figure.figsize': (12, 8),
    'legend.shadow': False,    
    'legend.fontsize': 10,
    'lines.linewidth': 2.0,
}

plt.rcParams.update(params)

fig, ax = plt.subplots(figsize=(6, 6))

# ponto
ax.scatter(z.real, z.imag, s=100, color='red')

# vetor
ax.quiver(0, 0, z.real, z.imag, units='xy', angles='xy', scale=1)

# identificando adequadamente o gráfico
ax.set_xlabel('Re')
ax.set_ylabel('Im')
fig.suptitle('Plano de Argand-Gauss', color='dimgray')
ax.set_title('Complexo: $z = 4 + 3i$')

plt.show()

Sendo um vetor, por definição o mesmo deve ter uma magnitude, ou módulo (o comprimento do vetor) e pode ser decomposto em seus vetores componentes. Vamos melhorar ainda mais nossa representação:

import math
from matplotlib.ticker import MultipleLocator
from matplotlib.patches import Arc

fig, ax = plt.subplots()

# construindo nosso sistema de coordenadas e limpando o gráfico
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_position('center')
ax.spines['left'].set_position('center')
ax.spines['right'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
ax.grid(which='both', color='grey', linewidth=1, linestyle='-', alpha=0.2)

# ponto e vetor
ax.scatter(z.real, z.imag, s=100, color='red')
ax.quiver(0, 0, z.real, z.imag, units='xy', angles='xy', scale=1)

# círculo
circle = plt.Circle((0, 0), 5, color='blue', fill=False)
ax.set_aspect('equal', adjustable='datalim')
ax.add_patch(circle)

# vetores componenentes
ax.quiver(0, 0, z.real, 0, units='xy', angles='xy', scale=1, color='red',
          width=0.075, alpha=0.25)
ax.quiver(0, 0, 0, z.imag, units='xy', angles='xy', scale=1, color='red',
          width=0.075, alpha=0.25)

# linhas para evidenciar os triângulos
ax.hlines(z.imag, 0, z.real, linestyle='--', color='purple')
ax.vlines(z.real, 0, z.imag, linestyle='--', color='purple')

# mostrando o ângulo
angle = math.degrees(math.asin(z.imag/abs(z)))  # valeu, Pitágoras!

arc = Arc(xy=(0, 0), width=2, height=2, 
          theta1=0, theta2=angle,
          linewidth=2)
ax.add_patch(arc)

ax.text(1.1, 0.4, f'{angle:.1f}°', fontsize=14)

# identificando adequadamente o gráfico
ax.set_xlabel('Re', loc='right')
ax.set_ylabel('Im', rotation='horizontal', ha='right', y=0.96, labelpad=-50)
fig.suptitle('Plano de Argand-Gauss', color='dimgray')
ax.set_title('Complexo: $z = 4 + 3i$')

plt.show()

Em nossa representação, vemos que a magnitude do vetor é 5 (veja como o vetor rotacionado ao redor da origem fornece uma circunferência de raio 5, apresentada em azul). Podemos chegar à mesma conclusão enxergando que se forma um triângulo retângulo, como as linhas tracejadas no gráfico buscam ressaltar. Fica evidente que o vetor é a hipotenusa do triângulo e o valor pode ser encontrado pelo Teorema de Pitágoras.

A função abs do Python pode ser utilizada para obter o valor:

abs(z)

Podemos verificar que é o mesmo valor obtido pelo Teorema de Pitágoras a partir do comprimento dos vetores componentes:

abs(z) == math.sqrt(z.real**2 + z.imag**2)
True

Um outro aspecto relacionado com o gráfico e que vale resgatar: o complexo pode ser representado por um ponto. O Python sabe disso:

complex(4, 3) == 4 + 3j
True

Ou seja, ao invés de escrever por extenso a forma algébrica, basta passar para a função complex a parte real e a parte imaginária como se fossem um par ordenado.

Volte no gráfico anterior. Veja que deixei evidenciado o ângulo formado pelo vetor com o eixo horizontal. Ora, se temos um ângulo e uma representação triangular, podemos utilizar funções trigonométricas, como já vimos em outro artigo desta série sobre SymPy.

O número complexo pode, portanto, ser representado no que chamamos de forma trigonométrica:

z = \rho (\cos \theta + i \sin \theta)

onde \rho representa o módulo do vetor.

Bom, seria interessante se pudéssemos passar da forma algébrica para a forma trigonométrica com pouco esforço. E, claro, é possível. Veja, matematicamente é bem simples passar de uma forma para outra, basta aplicar um pouco de Pitágoras (aliás, veja o código que gerou o gráfico para uma demonstração disso e para um easter egg). O que buscamos aqui é uma forma de fazer isso com código.

Antes, vamos ver como a biblioteca math, que já vem inclusa no Python, lida com a seguinte situação:

math.sqrt(-4)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_132990/3044196321.py in <module>
----> 1 math.sqrt(-4)

ValueError: math domain error

OK, como era esperado, apresentou um erro por não saber calcular a raiz quadrada de um número negativo. Mas sabemos que é possível calcular, apenas será complexa e não real. Assim, também há a biblioteca padrão cmath para lidar com situações onde números complexos podem estar envolvidos. Vejamos:

import cmath

cmath.sqrt(-4)
2j

Bingo! Calculou corretamente, apresentando o resultado complexo.

A cmath possui um método, o polar que nos fornece justamente o que buscamos:

cmath.polar(z)

Vamos entender o resultado por partes. Primeiro, o nome do método: polar. Um outro nome para a forma trigonométrica é forma polar. Isto porque um número complexo pode ser representado em um sistema de coordenadas polares. Matematicamente, se algo pode ser representado por um ponto em um plano com uma dada distância até um ponto de referência e um dado ângulo contra uma direção de referência então, pode ser representado em um sistema de coordenadas polares. É exatamente nossa situação. Se não ficou claro, mais adiante veremos uma representação.

O que o método retornou? Ora, a distância até o ponto de referência (nosso módulo) e o valor do ângulo. Acontece que o valor do ângulo está em radianos e não em graus como no gráfico. Se convertermos veremos que são iguais:

math.degrees(cmath.polar(z)[1])

O código acima pode ser simplificado se soubermos que o ângulo é também chamado de fase (phase) e, se arredondarmos o resultado para a primeira casa decimal, temos exatamente o mesmo ângulo do gráfico:

round(math.degrees(cmath.phase(z)), 1)

Caso ainda não esteja convencido, vamos fazer a conta por extenso da forma trigonométrica/polar e observar que teremos a forma algébrica como resultado:

abs(z) * (math.cos(cmath.phase(z)) + math.sin(cmath.phase(z))*1j)
(4+3j)

Afinal, é apenas aplicar o módulo e o ângulo:

cmath.polar(z) == (abs(z), cmath.phase(z))
True

Convencido?

Para terminar esta seção, vejamos a representação de nosso complexo exemplo em um sistema de coordenadas polares:

fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}, figsize=(6, 6))

ax.plot(cmath.phase(z), abs(z), marker='o', markersize=15, color='red')
plt.show()

Veja que até o momento utilizamos apenas o que o Python puro nos fornece.
O que será que o SymPy nos agrega?

Números complexos com SymPy

Bom, comecemos importando algumas coisas que utilizaremos:

from sympy import I, re, im, Abs, arg, conjugate, solve, Symbol

A representação do SymPy para a unidade imaginária é I:

I

Trata-se de um representação interna do próprio SymPy:

type(I)
sympy.core.numbers.ImaginaryUnit

Como o SymPy é para álgebra simbólica, vemos que a multiplicação de duas unidades imaginárias é efetivamente -1:

I * I

Vamos recriar nosso complexo que estamos utilizando de exemplo desde o início do artigo mas agora como um símbolo do SymPy:

z = 4 + 3 * I
z

Ao invés de tratar a parte imaginária e a parte real como atributos do objeto, o SymPy possui funções para as quais passamos um complexo e tais funções retornam as partes. São as funções re e im para as partes real e imaginária, respectivamente:

re(z)
im(z)

Da mesma forma, há Abs para obter o módulo, e conjugate para obter o conjugado:

Abs(z)
conjugate(z)

Vimos anteriormente que um complexo pode ser representado em coordenadas polares a partir de seu módulo e sua fase, o ângulo que faz com um eixo de referência. Outro nome para tal ângulo é argumento. Assim, há a função arg para obter o argumento de um complexo:

arg(z)

Veja a diferença de uma biblioteca simbólica. Efetivamente o SymPy retornou que o ângulo é aquele cujo valor da tangente é 3/4. Volte para o gráfico que fizemos anteriormente e você efetivamente verá que realmente é o ângulo de tangente 3/4.

Caso queira o valor por extenso, use o método n como mostrado no primeiro artigo dessa série para obter uma aproximação numérica:

arg(z).n()

O resultado sai em radianos. Para obter em graus e representar com a mesma quantidade de significativos apresentada anteriormente no gráfico fazemos:

from sympy import deg

deg(arg(z)).n(3)

Agora, vejamos o real poder de uma biblioteca simbólica.

Fórmula de Euler

Em um recente artigo sobre funções trigonométricas, vimos que estas podem ser escritas a partir de funções exponenciais com a chamada fórmula de Euler:

e^{i \theta} = \cos(\theta) + i \sin(\theta)

Vejamos como o SymPy reconhece essa igualdade. Primeiro, vamos representar o lado esquerdo da igualdade:

from sympy import exp, sin, cos, symbols

# SymPy reconhece símbolos LaTeX :-)
rho, theta = symbols(r'\rho \theta', real=True)
exp(I * theta)

Agora, vamos utilizar o método expand, passando a informação de que é para considerar números complexos:

exp(I * theta).expand(complex=True)

Veja que é exatamente o lado direito da igualdade.

E o contrário? Dado o lado direito da equação, podemos solicitar que o mesmo seja simplificado:

(cos(theta) + I * sin(theta)).simplify()

Obtivemos exatamente o lado esquerdo da igualdade.

Podemos multiplicar os dois lados da igualdade por qualquer valor. Vamos multiplicar pelo símbolo \rho, que estamos utilizando para representar módulo:

\rho e^{i \theta} = \rho (\cos(\theta) + i \sin(\theta))

Ora, o lado direito se tornou a representação na forma polar de um complexo genérico, o que já que vimos anteriormente.

Vamos, então, tentar obter a forma algébrica de um complexo a partir de sua forma exponencial. Vamos utilizar o complexo 4 + 3i, nosso exemplo desde o início. Já sabemos o módulo e o argumento para tal complexo, de forma que podemos substituir os valores obtendo uma expressão para o lado esquerdo da igualdade:

from sympy import atan
expr = (rho * exp(I * theta)).subs({rho: 5, theta: atan('3/4')})
expr

O SymPy possui o método as_real_imag que, como o nome sugere, tenta representar o objeto como uma tupla (parte real, parte imaginária). Ou seja, como um ponto no plano, conforme visto anteriormente no artigo. Vejamos o resultado de tal método:

expr.as_real_imag()

Exatamente o esperado! Ou seja, o SymPy eleva a outro nível a capacidade de trabalhar com expressões complexas. Com Python puro, precisamos conhecer as relações e construí-las manualmente. Com o SymPy, as transformações podem ser feitas facilmente, com simples chamadas de métodos.

Conclusão, mais complexos, e mais SymPy

Números complexos não é um assunto tão complexo 😉 Com as ferramentas adequadas e visualizações o assunto se torna mais palpável. Acompanhe o projeto Ciência Programada nas redes sociais para saber quando são publicados novos artigos, em breve escreverei mais sobre complexos, aguarde por gráficos bonitos 🙂

A lista completa de artigos sobre SymPy pode ser vista na tag SymPy aqui do site.

Até a próxima

Compartilhe:

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Rolar para cima