Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Caderno 4

"Divide et impera." (Júlio César)

Objetivos de aprendizagem

  • Compreender o funcionamento do método da bisseção, seu algoritmo e as condições necessárias para sua aplicação;

  • Aplicar o método da bisseção na resolução de equações não lineares;

  • Analisar o comportamento da convergência do método da bisseção, sua precisão e limitações.

Método da Bisseção: A Arte de Cindir e Convergir

O método da bisseção (MB) é uma das técnicas mais fundamentais para busca de raízes de equações não-lineares. Robusto e de fácil implementação, ele está estruturado sobre o Teorema do Valor Intermediário, que afirma que se uma função contínua f(x)f(x) tem sinais opostos em dois pontos aa e bb, condição que se verifica quando f(a)f(b)<0f(a) f(b) < 0 é verdadeira, então existe pelo menos uma raiz da função no intervalo [a,b][a, b]. Em termos simples, o método envolve dividir repetidamente o intervalo ao meio e selecionar o subintervalo onde a raiz deve estar, baseando-se nos sinais de ff.

O MB tem garantia de convergência, desde que a função seja contínua e o intervalo inicial seja escolhido corretamente. No entanto, a convergência pode ser lenta em comparação com outros métodos, especialmente quando a função tiver múltiplas raízes próximas. Neste capítulo, apresentaremos uma implementação funcional do método da bisseção para equações não-lineares unidimensionais. O algoritmo é capaz de lidar com as principais funções matemáticas da biblioteca numpy, possui alguns comandos para checagem de validade e usa a estrutura while para o laço do processo iterativo.

Explicação do algoritmo

O algoritmo funciona da seguinte forma: calcula-se o ponto médio do intervalo atual,

c=a+b2,c = \frac{a + b}{2},

e avalia-se o sinal de f(c)f(c). Se f(a)f(c)<0f(a) f(c) < 0, a raiz está no intervalo [a,c][a, c]; caso contrário, está em [c,b][c, b]. O intervalo é então atualizado, e o processo é repetido até que o tamanho do intervalo ba|b - a| seja menor que uma tolerância ϵ\epsilon definida previamente, ou até que f(c)f(c) seja suficientemente próximo de zero.

Pseudocódigo

Entrada: função f(x), intervalo [a, b], tolerância ε, número máximo de iterações N Saída: aproximação da raiz ou mensagem de erro

  1. Verifique se f(a)f(b)<0f(a)f(b) < 0 e avance. Se não, lance erro (sem garantia de raiz no intervalo) e pare.

  2. Para kk de 1 até NN:

    a. c(a+b)/2c \leftarrow (a + b) / 2 (ponto médio do intervalo)

    b. Se (ba)/2<ϵ(b - a)/2 < \epsilon: Retorne cc como raiz aproximada

    c. Se f(a)f(c)<0f(a)f(c) < 0: bcb \leftarrow c Senão: aca \leftarrow c

  3. Lance mensagem de número máximo de iterações atingido e retorne cc como melhor estimativa

Implementação do algoritmo

No algoritmo proposto abaixo, a função bissecao requererá 5 argumentos:

  • a função f(x)f(x) propriamente dita, representada por f;

  • duas estimativas iniciais a e b, que representam o intervalo de busca da raiz [a,b][a,b];

  • o erro absoluto desejado EAdEA_d, representado por e;

  • o número máximo de iterações NN para tentativa de solução, representado por N.

Source
import inspect, re 
import numpy as np
from matplotlib.pyplot import plot
from warnings import warn
from prettytable import PrettyTable as pt

def bissecao(f,a,b,tol,N):
    """Algoritmo para determinação de raízes de equações não lineares 
       unidimensionais pelo método da bisseção.

    Parâmetros: 
        f: string dependendo de uma variável, i.e., a função-alvo
            (e.g., 'x**2 - 1', 'x**2*cos(x)', etc.) 
        a: estimativa inferior
        b: estimativa superior
        tol: erro desejado (tolerância)
        N: número máximo de iterações a repetir

    Retorno: 
        x: aproximação para a raiz da função   
    """
    
    # construtor de tabela
    table = pt()
    
    # substitui expressões da string pelas chamadas das funções do numpy
    f = re.sub('(sin|sinh|cos|cosh|tan|tanh|exp|log|sqrt|log10|arcsin|arccos|arctan|arcsinh|arccosh|arctanh)', r'np.\1', f)
    
    # identifica a variável independente
    var = re.search(r'([a-zA-Z][\w]*) ?([\+\-\/*]|$|\))', f).group(1)
    
    # cria função anônima
    f = eval('lambda ' + var + ' :' + f)
    
    # checa se a função é de uma variável, senão lança erro        
    if len(inspect.getfullargspec(f).args) - 1 > 0:    
        raise ValueError('O código é válido apenas para uma variável.')

    # calcula valor da função nos extremos
    fa = f(a) 
    fb = f(b)
    
    # verifica sinal da função para o intervalo passado     
    if fa*fb >= 0:
        raise ValueError('A função deve ter sinais opostos em a e b!')
    
    # flag usada para marcar caso f(xm) = 0
    done = False;
        
    # no. iterações mínimo
    niter = int(np.ceil(np.log((b-a)/tol)/np.log(2)))
    if N < niter:
        print(f'São necessárias pelo menos {niter} iterações, mas N = {N}.\n')

    
    # cabeçalho de tabela
    table.field_names = ['i','xm','f(xm)','a','b','f(a)','f(b)','EA']

    # bisecta o intervalo
    xm = (a+b)/2
    
    # contador 
    i = 1 
    
    # loop 
    while abs(a-b) > tol and (not done and N != 0):    
        
        # avalia a função no ponto médio
        fxm = f(xm) 
                        
        # adiciona linha de tabela de resultado
        table.add_row([i,np.round(xm,8),np.round(f(xm),8),
                   np.round(a,4),np.round(b,4),
                   np.round(f(a),4),np.round(f(b),4),
                   f'{abs(a-b):e}'])
   
        if fa*fxm < 0:      # Raiz esta à esquerda de xm
            b = xm
            fb = fxm
            xm = (a+b)/2
        elif fxm*fb < 0:    # Raiz esta à direita de xm
            a = xm
            fa = fxm
            xm = (a+b)/2
        else:               # Achamos a raiz
            done = True            
    
        N -= 1              # Atualiza passo
        i += 1              # Atualiza contador
    
    # impressão de tabela
    table.add_row([i,np.round(xm,8),np.round(f(xm),8),
                   np.round(a,4),np.round(b,4),
                   np.round(f(a),4),np.round(f(b),4),
                   f'{abs(a-b):e}'])
    table.align = 'c'; print(table)
    
    return xm

Note que o cerne desta função é o trecho abaixo. O resto do código é constituído de algumas especializações e comandos acessórios.

while abs(a-b) > tol and (not done and N != 0):    
    
    # avalia a função no ponto médio
    fxm = f(xm) 

    if fa*fxm < 0:      # Raiz esta à esquerda de xm
        b = xm
        fb = fxm
        xm = (a+b)/2
    elif fxm*fb < 0:    # Raiz esta à direita de xm
        a = xm
        fa = fxm
        xm = (a+b)/2
    else:               # Achamos a raiz
        done = True            

    N -= 1              # Atualiza passo
    i += 1              # Atualiza contador

Playground interativo

Utilize o playground interativo abaixo para testar o método da bisseção para funções não lineares quaisquer. Como dado de entrada para f(x)f(x), utilize funções escritas nos moldes de Python científico como um tipo str. Para os parâmetros do intervalo inicial e de erro, utilize float.

Loading...

Aplicações

Exemplo: Resolva o problema f(x)=0f(x) = 0, para f(x)=arccos(x)+4sen(x)+1.7f(x) = -\text{arccos}(x) + 4\text{sen}(x) + 1.7, no intervalo 0.2x1.0-0.2 \le x \le 1.0 e ϵ=103\epsilon = 10^{-3}.

  • Primeiramente, façamos uma análise gráfica para verificar o comportamento da função.

x = np.linspace(-0.2,1,100)
plot(x,-np.arccos(x) + 4*np.sin(x) + 1.7,'g',x,0*x,'k:');
<Figure size 640x400 with 1 Axes>
  • Uma vez que a raiz é única, basta aplicar o método que construímos à função desejada.

xm = bissecao('-arccos(x) + 4*sin(x) + 1.7',-0.2,0.2,1e-9,30)
+----+-------------+-------------+---------+---------+---------+--------+--------------+
| i  |      xm     |    f(xm)    |    a    |    b    |   f(a)  |  f(b)  |      EA      |
+----+-------------+-------------+---------+---------+---------+--------+--------------+
| 1  |     0.0     |  0.12920367 |   -0.2  |   0.2   | -0.8668 | 1.1252 | 4.000000e-01 |
| 2  |     -0.1    | -0.37029741 |   -0.2  |   0.0   | -0.8668 | 0.1292 | 2.000000e-01 |
| 3  |    -0.05    | -0.12073386 |   -0.1  |   0.0   | -0.3703 | 0.1292 | 1.000000e-01 |
| 4  |    -0.025   |  0.00421148 |  -0.05  |   0.0   | -0.1207 | 0.1292 | 5.000000e-02 |
| 5  |   -0.0375   | -0.05826997 |  -0.05  |  -0.025 | -0.1207 | 0.0042 | 2.500000e-02 |
| 6  |   -0.03125  | -0.02703107 | -0.0375 |  -0.025 | -0.0583 | 0.0042 | 1.250000e-02 |
| 7  |  -0.028125  | -0.01141021 | -0.0312 |  -0.025 |  -0.027 | 0.0042 | 6.250000e-03 |
| 8  |  -0.0265625 | -0.00359946 | -0.0281 |  -0.025 | -0.0114 | 0.0042 | 3.125000e-03 |
| 9  | -0.02578125 |  0.00030599 | -0.0266 |  -0.025 | -0.0036 | 0.0042 | 1.562500e-03 |
| 10 | -0.02617188 | -0.00164674 | -0.0266 | -0.0258 | -0.0036 | 0.0003 | 7.812500e-04 |
| 11 | -0.02597656 | -0.00067038 | -0.0262 | -0.0258 | -0.0016 | 0.0003 | 3.906250e-04 |
| 12 | -0.02587891 | -0.00018219 |  -0.026 | -0.0258 | -0.0007 | 0.0003 | 1.953125e-04 |
| 13 | -0.02583008 |   6.19e-05  | -0.0259 | -0.0258 | -0.0002 | 0.0003 | 9.765625e-05 |
| 14 | -0.02585449 |  -6.015e-05 | -0.0259 | -0.0258 | -0.0002 | 0.0001 | 4.882812e-05 |
| 15 | -0.02584229 |   8.8e-07   | -0.0259 | -0.0258 | -0.0001 | 0.0001 | 2.441406e-05 |
| 16 | -0.02584839 |  -2.964e-05 | -0.0259 | -0.0258 | -0.0001 |  0.0   | 1.220703e-05 |
| 17 | -0.02584534 |  -1.438e-05 | -0.0258 | -0.0258 |   -0.0  |  0.0   | 6.103516e-06 |
| 18 | -0.02584381 |  -6.75e-06  | -0.0258 | -0.0258 |   -0.0  |  0.0   | 3.051758e-06 |
| 19 | -0.02584305 |  -2.94e-06  | -0.0258 | -0.0258 |   -0.0  |  0.0   | 1.525879e-06 |
| 20 | -0.02584267 |  -1.03e-06  | -0.0258 | -0.0258 |   -0.0  |  0.0   | 7.629395e-07 |
| 21 | -0.02584248 |    -8e-08   | -0.0258 | -0.0258 |   -0.0  |  0.0   | 3.814697e-07 |
| 22 | -0.02584238 |    4e-07    | -0.0258 | -0.0258 |   -0.0  |  0.0   | 1.907349e-07 |
| 23 | -0.02584243 |   1.6e-07   | -0.0258 | -0.0258 |   -0.0  |  0.0   | 9.536743e-08 |
| 24 | -0.02584245 |    4e-08    | -0.0258 | -0.0258 |   -0.0  |  0.0   | 4.768372e-08 |
| 25 | -0.02584246 |    -2e-08   | -0.0258 | -0.0258 |   -0.0  |  0.0   | 2.384186e-08 |
| 26 | -0.02584246 |    1e-08    | -0.0258 | -0.0258 |   -0.0  |  0.0   | 1.192093e-08 |
| 27 | -0.02584246 |     -0.0    | -0.0258 | -0.0258 |   -0.0  |  0.0   | 5.960464e-09 |
| 28 | -0.02584246 |     0.0     | -0.0258 | -0.0258 |   -0.0  |  0.0   | 2.980232e-09 |
| 29 | -0.02584246 |     -0.0    | -0.0258 | -0.0258 |   -0.0  |  0.0   | 1.490116e-09 |
| 30 | -0.02584246 |     0.0     | -0.0258 | -0.0258 |   -0.0  |  0.0   | 7.450581e-10 |
+----+-------------+-------------+---------+---------+---------+--------+--------------+
  • A raiz aproximada xx^{*}, tal que para f(x)=0f(x^{*}) = 0 no intervalo-alvo é mostrada na última linha da tabela. Isto é,

# raiz aproximada
xm
-0.025842459872364998

Exemplo: Resolva o problema h(z)=0h(z) = 0, para h(z)=z(12z)1tan(z+1)h(z) = -z(1-2z)^{-1} - \text{tan}(z+1), no intervalo [1,8][1,8] e ϵ=105\epsilon = 10^{-5}.

  • Primeiramente, façamos uma análise gráfica para verificar o comportamento da função.

z = np.linspace(1,8,100)
plot(z,z/(1 - 2*z) - np.tan(z+1),'g',z,0*z,'k:');
<Figure size 640x400 with 1 Axes>
  • Neste caso, a função apresenta sensibilidades e mais de uma raiz no intervalo dado. Vamos buscar a raiz que está no subintervalo [4,6][4,6]. Para tanto, vamos deamplificar a plotagem.

z = np.linspace(4,6,100)
plot(z,z/(1 - 2*z) - np.tan(z+1),'g',z,0*z,'k:');
<Figure size 640x400 with 1 Axes>
  • Uma vez que a raiz é única, basta aplicar o método que construímos à função desejada.

zm = bissecao('z/(1 - 2*z) - tan(z+1)',4,6,1e-5,20)
+----+------------+-------------+--------+--------+--------+---------+--------------+
| i  |     xm     |    f(xm)    |   a    |   b    |  f(a)  |   f(b)  |      EA      |
+----+------------+-------------+--------+--------+--------+---------+--------------+
| 1  |    5.0     | -0.26454936 |   4    |   6    | 2.8091 | -1.4169 | 2.000000e+00 |
| 2  |    4.5     |  0.43308405 |   4    |  5.0   | 2.8091 | -0.2645 | 1.000000e+00 |
| 3  |    4.75    |  0.03138032 |  4.5   |  5.0   | 0.4331 | -0.2645 | 5.000000e-01 |
| 4  |   4.875    | -0.12466745 |  4.75  |  5.0   | 0.0314 | -0.2645 | 2.500000e-01 |
| 5  |   4.8125   | -0.04914268 |  4.75  | 4.875  | 0.0314 | -0.1247 | 1.250000e-01 |
| 6  |  4.78125   | -0.00957612 |  4.75  | 4.8125 | 0.0314 | -0.0491 | 6.250000e-02 |
| 7  |  4.765625  |  0.01071879 |  4.75  | 4.7812 | 0.0314 | -0.0096 | 3.125000e-02 |
| 8  | 4.7734375  |  0.00052675 | 4.7656 | 4.7812 | 0.0107 | -0.0096 | 1.562500e-02 |
| 9  | 4.77734375 | -0.00453568 | 4.7734 | 4.7812 | 0.0005 | -0.0096 | 7.812500e-03 |
| 10 | 4.77539062 | -0.00200723 | 4.7734 | 4.7773 | 0.0005 | -0.0045 | 3.906250e-03 |
| 11 | 4.77441406 | -0.00074094 | 4.7734 | 4.7754 | 0.0005 |  -0.002 | 1.953125e-03 |
| 12 | 4.77392578 | -0.00010727 | 4.7734 | 4.7744 | 0.0005 | -0.0007 | 9.765625e-04 |
| 13 | 4.77368164 |  0.0002097  | 4.7734 | 4.7739 | 0.0005 | -0.0001 | 4.882812e-04 |
| 14 | 4.77380371 |   5.12e-05  | 4.7737 | 4.7739 | 0.0002 | -0.0001 | 2.441406e-04 |
| 15 | 4.77386475 |  -2.803e-05 | 4.7738 | 4.7739 | 0.0001 | -0.0001 | 1.220703e-04 |
| 16 | 4.77383423 |  1.158e-05  | 4.7738 | 4.7739 | 0.0001 |   -0.0  | 6.103516e-05 |
| 17 | 4.77384949 |  -8.23e-06  | 4.7738 | 4.7739 |  0.0   |   -0.0  | 3.051758e-05 |
| 18 | 4.77384186 |   1.68e-06  | 4.7738 | 4.7738 |  0.0   |   -0.0  | 1.525879e-05 |
| 19 | 4.77384567 |  -3.27e-06  | 4.7738 | 4.7738 |  0.0   |   -0.0  | 7.629395e-06 |
+----+------------+-------------+--------+--------+--------+---------+--------------+
  • A raiz aproximada zz^{*}, tal que para h(z)=0h(z^{*}) = 0 é mostrada na última linha da tabela. Isto é,

# raiz aproximada
zm
4.773845672607422

Por fim, vamos aplicar nosso método da bisseção ao problema do paraquedista apresentado no capítulo introdutório para buscar o coeficiente de arrasto adequado para os parâmetros de projeto impostos.

Primeiramente, definiremos uma função para retornar a equação particular.

Source
def eq_paraq(tempo,massa,vel,grav):
    """ Define equação particular para cálculo de coeficiente de arrasto 
        em salto de paraquedista. (Ver introdução.)
        
        Parâmetros: 
        
            tempo: duração de salto até velocidade terminal [s]
            massa: massa do paraquedista [kg]
            vel: velocidade terminal desejada [m/s]
            grav: aceleração gravitacional ambiente [m/s**2]
        
        Retorno: 
        
            f: expressão numérica para equação particular
               do coeficiente de arrasto
                
    """
    
    # variáveis simbólicas    
    from sympy import symbols, exp, lambdify
    
    g,m,t,v,c = symbols('g,m,t,v,c')
    
    # expressão geral
    f = (g*m/c)*(1 - exp((-c/m)*t)) - v
    
    # expressão particular com valores substituídos convertidos para str
    fs = f.subs({'g':grav,'m':massa,'v':vel,'t':tempo})
    
    # expressão simbólica convertida para expessão numérica
    fn = lambdify(c,fs,"numpy")
    
    # imprime para 
    print(f'Equação particular: f(c) = {fs}')
    
    return (fs,fn)

Em seguida, inserimos valores de entrada para teste.

# parâmetros de entrada
tempo, massa, vel, grav = 12, 70, 42, 9.81

# equação particular
fs,fn = eq_paraq(tempo,massa,vel,grav)
Equação particular: f(c) = -42 + 686.7*(1 - exp(-6*c/35))/c

O próximo passo realiza a análise gráfica para localização do intervalo de aproximação da raiz.

c = np.linspace(1,20)
plot(c,fn(c),'g',c,0*c,'k:');
<Figure size 640x400 with 1 Axes>

Encerrando, chamamos a função.

cm = bissecao(str(fs),14,17.0,1e-4,20)
+----+-------------+-------------+---------+---------+--------+---------+--------------+
| i  |      xm     |    f(xm)    |    a    |    b    |  f(a)  |   f(b)  |      EA      |
+----+-------------+-------------+---------+---------+--------+---------+--------------+
| 1  |     15.5    | -0.80457281 |    14   |   17.0  | 2.6003 |  -3.797 | 3.000000e+00 |
| 2  |    14.75    |  0.84203047 |    14   |   15.5  | 2.6003 | -0.8046 | 1.500000e+00 |
| 3  |    15.125   |  0.00533671 |  14.75  |   15.5  | 0.842  | -0.8046 | 7.500000e-01 |
| 4  |   15.3125   | -0.40289836 |  15.125 |   15.5  | 0.0053 | -0.8046 | 3.750000e-01 |
| 5  |   15.21875  | -0.19960925 |  15.125 | 15.3125 | 0.0053 | -0.4029 | 1.875000e-01 |
| 6  |  15.171875  | -0.09734443 |  15.125 | 15.2188 | 0.0053 | -0.1996 | 9.375000e-02 |
| 7  |  15.1484375 | -0.04605603 |  15.125 | 15.1719 | 0.0053 | -0.0973 | 4.687500e-02 |
| 8  | 15.13671875 | -0.02037272 |  15.125 | 15.1484 | 0.0053 | -0.0461 | 2.343750e-02 |
| 9  | 15.13085938 | -0.00752127 |  15.125 | 15.1367 | 0.0053 | -0.0204 | 1.171875e-02 |
| 10 | 15.12792969 |  -0.0010931 |  15.125 | 15.1309 | 0.0053 | -0.0075 | 5.859375e-03 |
| 11 | 15.12646484 |  0.0021216  |  15.125 | 15.1279 | 0.0053 | -0.0011 | 2.929688e-03 |
| 12 | 15.12719727 |  0.0005142  | 15.1265 | 15.1279 | 0.0021 | -0.0011 | 1.464844e-03 |
| 13 | 15.12756348 | -0.00028946 | 15.1272 | 15.1279 | 0.0005 | -0.0011 | 7.324219e-04 |
| 14 | 15.12738037 |  0.00011237 | 15.1272 | 15.1276 | 0.0005 | -0.0003 | 3.662109e-04 |
| 15 | 15.12747192 |  -8.855e-05 | 15.1274 | 15.1276 | 0.0001 | -0.0003 | 1.831055e-04 |
| 16 | 15.12742615 |  1.191e-05  | 15.1274 | 15.1275 | 0.0001 | -0.0001 | 9.155273e-05 |
+----+-------------+-------------+---------+---------+--------+---------+--------------+

Como se vê, o coeficiente de arrasto aproximado para este caso é dado por:

cm
15.127426147460938

Problemas propostos

Para cada problema, dispondo de seu enunciado e informações, siga os 3 passos a seguir:

  1. Faça a análise gráfica do modelo matemático do problema.

  2. Defina o(s)intervalo(s) adequados(s) de localização.

  3. Aplique o método da bisseção.

Problema 1. Uma reação química reversível 2A+B    C2A+B \iff C pode ser caracterizada pela relação de equilíbrio K=ccca2cbK = \dfrac{c_c}{c_a^2c_b}, onde cic_i representa a concentração do constituinte ii. Suponha que:

  • xx é o número de moles de CC que são produzidos

  • a conservação da massa pode ser usada para reformular a relação de equilíbrio como

K=(cc,0+x)(ca,02x)2(cb,0x),K = \dfrac{(c_{c,0} + x)}{(c_{a,0} - 2x)^2 (c_{b,0} - x)},

onde o subscrito 0 designa a concentração inicial de cada constituinte.

  • K=0,016K = 0,016, ca,0=42c_{a,0} = 42, cb,0=28c_{b,0} = 28 e cc,0=4c_{c,0} = 4.

Determine uma aproximação para xx com erro inferior a 10-5.

Problema 2. A potência de saída de uma célula solar varia com a tensão que ela fornece. A saída VmpV_{mp} para a qual a potência de saída é máxima é dada pela equação:

exp(qVmp/kBT)(1+qVmpkBT)=exp(qVoc/kBT)\text{exp}(qV_{mp}/k_BT) \Big( 1 + \dfrac{qV_{mp}}{k_B T} \Big) = \text{exp}(qV_{oc}/k_B T)

onde:

  • VocV_{oc} é a tensão de circuito aberto [V];

  • TT é a temperatura [K];

  • qq = 1.6022e-19 [C] é a carga de um elétron;

  • kk = 1.3806e23 [J/K] é a constante de Boltzmann.

Para VocV_{oc} = 0.5 V e uma temperatura TT = 297 K, determine a tensão VmpV_{mp} na qual a potência de saída da célula solar é máxima.