3  Zeri di funzione (esempi)

Autore/Autrice
Affiliazione

Enrico Bertolazzi

University of Trento, Department of Industrial Engineering

Alcuni esempi di problemi che si risolvono trovando le radici di f(x)=0.

ConsiglioSolutori in R

Metodo di bisezione per trovare la radice

bisection_method <- function(func, lower, upper, tol = 1e-6, max_iter = 100) {
  for (i in 1:max_iter) {
    cat(sprintf("Intervallo: lower = %g, upper = %g\n", lower, upper))
    midpoint <- (lower + upper) / 2
    if (func(midpoint) == 0 || (upper - lower) / 2 < tol) {
      return(midpoint)  # Restituisce il punto medio se è la radice
    }
    if (sign(func(midpoint)) == sign(func(lower))) {
      lower <- midpoint  # Aggiorna l'intervallo inferiore
    } else {
      upper <- midpoint  # Aggiorna l'intervallo superiore
    }
  }
  return(NULL)  # Se non si trova la radice
}

Metodo delle secanti per trovare la radice

secant_method <- function(func, x0, x1, tol = 1e-6, max_iter = 100) {
  for (i in 1:max_iter) {
    if (abs(x1 - x0) < tol) {
      return(x1)  # Restituisce la soluzione se la differenza è inferiore alla tolleranza
    }
    x2 <- x1 - func(x1) * (x1 - x0) / (func(x1) - func(x0))  # Calcola il nuovo punto
    cat(sprintf("Iterazione %d: x = %g\n", i, x2))
    x0 <- x1
    x1 <- x2
  }
  warning("La radice non è stata trovata dopo il numero massimo di iterazioni.")
  return(NULL)  # Se non si trova la radice
}

Metodo di Newton-Raphson1 per trovare la radice

newton_method <- function(func, func_prime, initial_guess, tol = 1e-6, max_iter = 100) {
  x <- initial_guess
  for (i in 1:max_iter) {
    x_new <- x - func(x) / func_prime(x)  # Calcola il nuovo punto
    cat(sprintf("Iterazione %d: x = %g\n", i, x_new))
    if (abs(x_new - x) < tol) {
      return(x_new)  # Restituisce la soluzione se la differenza è inferiore alla tolleranza
    }
    x <- x_new
  }
  warning("La radice non è stata trovata dopo il numero massimo di iterazioni.")
  return(NULL)  # Se non si trova la radice
}

3.1 Calcolo della radice cubica

Il calcolo delle radici cubiche è essenziale in ingegneria e in scienze applicate, poiché molte grandezze fisiche e ingegneristiche, come il volume e la densità, richiedono la determinazione di dimensioni lineari a partire da quantità cubiche. In questo documento, analizzeremo il problema di trovare la radice cubica di un numero positivo a risolvendo l’equazione:

f(x) = x^3 - a = 0

Dove a rappresenta il volume di un cubo, e vogliamo determinare la lunghezza del lato x.

Soluzione

Consideriamo il problema di trovare la radice cubica di un numero a = 27. L’equazione che dobbiamo risolvere è:

f(x) = x^3 - 27 = 0

Stiamo cercando il valore di x tale che x^3 = 27.

Grafico della funzione

Possiamo visualizzare il grafico della funzione per identificare la posizione della radice:

# Grafico della funzione
library(ggplot2)

# Creare un vettore di valori x
x_values <- seq(-5, 5, by = 0.1)
f_values <- x_values^3 - 27

# Creazione del grafico
ggplot(data = data.frame(x = x_values, f = f_values), aes(x = x, y = f)) +
  geom_line(color = "blue") +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
  geom_vline(xintercept = 3, linetype = "dashed", color = "green") +
  labs(title = "Grafico della funzione $f(x) = x^3 - 27$",
       x = "$x$",
       y = "$f(x)$") +
  theme_minimal()

ConsiglioSoluzione in R

Definizione della funzione per il calcolo della radice cubica

radice_cubica <- function(x) {
  return(x*x*x-27)
}

Metodo di Bisezione

Trova la radice cubica di 27 usando il metodo di bisezione

sol <- bisection_method(radice_cubica, 0, 27)
Intervallo: lower = 0, upper = 27
Intervallo: lower = 0, upper = 13.5
Intervallo: lower = 0, upper = 6.75
Intervallo: lower = 0, upper = 3.375
Intervallo: lower = 1.6875, upper = 3.375
Intervallo: lower = 2.53125, upper = 3.375
Intervallo: lower = 2.95312, upper = 3.375
Intervallo: lower = 2.95312, upper = 3.16406
Intervallo: lower = 2.95312, upper = 3.05859
Intervallo: lower = 2.95312, upper = 3.00586
Intervallo: lower = 2.97949, upper = 3.00586
Intervallo: lower = 2.99268, upper = 3.00586
Intervallo: lower = 2.99927, upper = 3.00586
Intervallo: lower = 2.99927, upper = 3.00256
Intervallo: lower = 2.99927, upper = 3.00092
Intervallo: lower = 2.99927, upper = 3.00009
Intervallo: lower = 2.99968, upper = 3.00009
Intervallo: lower = 2.99989, upper = 3.00009
Intervallo: lower = 2.99999, upper = 3.00009
Intervallo: lower = 2.99999, upper = 3.00004
Intervallo: lower = 2.99999, upper = 3.00001
Intervallo: lower = 2.99999, upper = 3
Intervallo: lower = 2.99999, upper = 3
Intervallo: lower = 3, upper = 3
Intervallo: lower = 3, upper = 3
print(sol)
[1] 3.000001

Metodo delle secanti

Trova la radice cubica di 27 usando il metodo delle secanti

sol <- secant_method(radice_cubica, x0=0, x1=27, max_iter=40)
Iterazione 1: x = 0.037037
Iterazione 2: x = 0.0740232
Iterazione 3: x = 2814.65
Iterazione 4: x = 0.0740266
Iterazione 5: x = 0.07403
Iterazione 6: x = 1642.33
Iterazione 7: x = 0.07404
Iterazione 8: x = 0.07405
Iterazione 9: x = 1641.59
Iterazione 10: x = 0.0740601
Iterazione 11: x = 0.0740701
Iterazione 12: x = 1640.7
Iterazione 13: x = 0.0740801
Iterazione 14: x = 0.0740901
Iterazione 15: x = 1639.81
Iterazione 16: x = 0.0741002
Iterazione 17: x = 0.0741102
Iterazione 18: x = 1638.92
Iterazione 19: x = 0.0741203
Iterazione 20: x = 0.0741303
Iterazione 21: x = 1638.03
Iterazione 22: x = 0.0741404
Iterazione 23: x = 0.0741504
Iterazione 24: x = 1637.14
Iterazione 25: x = 0.0741605
Iterazione 26: x = 0.0741706
Iterazione 27: x = 1636.26
Iterazione 28: x = 0.0741807
Iterazione 29: x = 0.0741908
Iterazione 30: x = 1635.37
Iterazione 31: x = 0.0742008
Iterazione 32: x = 0.0742109
Iterazione 33: x = 1634.48
Iterazione 34: x = 0.074221
Iterazione 35: x = 0.0742312
Iterazione 36: x = 1633.59
Iterazione 37: x = 0.0742413
Iterazione 38: x = 0.0742514
Iterazione 39: x = 1632.7
Iterazione 40: x = 0.0742615
Warning in secant_method(radice_cubica, x0 = 0, x1 = 27, max_iter = 40): La
radice non è stata trovata dopo il numero massimo di iterazioni.
print(sol)
NULL

non funziona proviamo con nuovi punti di partenza

sol <- secant_method(radice_cubica, x0=27/2, x1=27)
Iterazione 1: x = 11.5926
Iterazione 2: x = 10.2912
Iterazione 3: x = 7.33534
Iterazione 4: x = 5.77205
Iterazione 5: x = 4.49521
Iterazione 6: x = 3.69196
Iterazione 7: x = 3.2295
Iterazione 8: x = 3.04379
Iterazione 9: x = 3.00316
Iterazione 10: x = 3.00005
Iterazione 11: x = 3
Iterazione 12: x = 3
print(sol)
[1] 3

Metodo di Newton

Definizione della derivata della funzione

radice_cubica_derivative <- function(x) {
  return(3*x*x)
}

Trova la radice cubica di 27 usando il metodo di Newton

sol <- newton_method(radice_cubica, radice_cubica_derivative, initial_guess = 27)
Iterazione 1: x = 18.0123
Iterazione 2: x = 12.036
Iterazione 3: x = 8.08611
Iterazione 4: x = 5.52838
Iterazione 5: x = 3.98006
Iterazione 6: x = 3.22152
Iterazione 7: x = 3.01488
Iterazione 8: x = 3.00007
Iterazione 9: x = 3
Iterazione 10: x = 3
print(sol)
[1] 3

3.2 Equazione di radiazione di Stefan-Boltzmann (Trasferimento di calore)

La potenza irradiata da un corpo nero è data dalla legge di Stefan-Boltzmann:

P = \sigma A (T^4 - T_{\infty}^4)

Dove: - P = 500 \, \text{W} - A = 2 \, \text{m}^2 - T_{\infty} = 300 \, \text{K} - \sigma = 5.67 \times 10^{-8} \, \text{W/m}^2\text{K}^4

Trova la temperatura del corpo T risolvendo l’equazione:

f(T) = 5.67 \times 10^{-8} \cdot 2 \cdot (T^4 - 300^4) - 500

ConsiglioSoluzione in R

Definizione della funzione f(T):

f <- function(T) {
  P <- 500
  A <- 2
  sigma <- 5.67e-8
  return(sigma * A * (T^4 - 300^4) - P)
}

Metodo di Bisezione

Trova la temperatura usando il metodo di bisezione:

sol <- bisection_method(f, 300, 600)
Intervallo: lower = 300, upper = 600
Intervallo: lower = 300, upper = 450
Intervallo: lower = 300, upper = 375
Intervallo: lower = 300, upper = 337.5
Intervallo: lower = 318.75, upper = 337.5
Intervallo: lower = 328.125, upper = 337.5
Intervallo: lower = 332.812, upper = 337.5
Intervallo: lower = 332.812, upper = 335.156
Intervallo: lower = 333.984, upper = 335.156
Intervallo: lower = 333.984, upper = 334.57
Intervallo: lower = 334.277, upper = 334.57
Intervallo: lower = 334.424, upper = 334.57
Intervallo: lower = 334.424, upper = 334.497
Intervallo: lower = 334.424, upper = 334.46
Intervallo: lower = 334.424, upper = 334.442
Intervallo: lower = 334.424, upper = 334.433
Intervallo: lower = 334.428, upper = 334.433
Intervallo: lower = 334.431, upper = 334.433
Intervallo: lower = 334.431, upper = 334.432
Intervallo: lower = 334.431, upper = 334.432
Intervallo: lower = 334.431, upper = 334.432
Intervallo: lower = 334.431, upper = 334.432
Intervallo: lower = 334.431, upper = 334.431
Intervallo: lower = 334.431, upper = 334.431
Intervallo: lower = 334.431, upper = 334.431
Intervallo: lower = 334.431, upper = 334.431
Intervallo: lower = 334.431, upper = 334.431
Intervallo: lower = 334.431, upper = 334.431
Intervallo: lower = 334.431, upper = 334.431
print(sol)
[1] 334.4315

Metodo delle secanti

Trova la soluzione per la temperatura usando il metodo delle secanti:

sol <- secant_method(f, x0 = 500, x1 = 400 )
Iterazione 1: x = 364.523
Iterazione 2: x = 341.536
Iterazione 3: x = 335.308
Iterazione 4: x = 334.459
Iterazione 5: x = 334.432
Iterazione 6: x = 334.431
Iterazione 7: x = 334.431
print(sol)
[1] 334.4315

Metodo di Newton

Derivata della funzione f(T) rispetto a T

f_prime <- function(T) {
  A <- 2
  sigma <- 5.67e-8
  return(4 * sigma * A * T^3)
}

Trova la soluzione per la temperatura usando il metodo di Newton-Raphson

sol <- newton_method(f, f_prime, initial_guess = 500)
Iterazione 1: x = 400.018
Iterazione 2: x = 348.871
Iterazione 3: x = 335.303
Iterazione 4: x = 334.435
Iterazione 5: x = 334.431
Iterazione 6: x = 334.431
print(sol)
[1] 334.4315

3.3 Equazione di Colebrook (Flusso di fluidi)

L’equazione di Colebrook per il coefficiente di attrito f è definita come:

\dfrac{1}{\sqrt{f}} = -2 \log \left( \dfrac{\epsilon / D}{3.7} + \dfrac{2.51}{Re \cdot \sqrt{f}} \right)

Dove:

  • \epsilon / D = 0.005
  • Re = 1.5 \times 10^5

Trova il coefficiente di attrito f.

ConsiglioSoluzione in R

Definizione della funzione di Colebrook

colebrook <- function(f) {
  epsilon_D <- 0.005
  Re <- 1.5e5
  return(1/sqrt(f) + 2 * log10(epsilon_D/3.7 + 2.51/(Re * sqrt(f))))
}

Metodo di Bisezione

Trova il coefficiente di attrito usando il metodo di bisezione

sol <- bisection_method(colebrook, 0.01, 0.1)
Intervallo: lower = 0.01, upper = 0.1
Intervallo: lower = 0.01, upper = 0.055
Intervallo: lower = 0.01, upper = 0.0325
Intervallo: lower = 0.02125, upper = 0.0325
Intervallo: lower = 0.026875, upper = 0.0325
Intervallo: lower = 0.0296875, upper = 0.0325
Intervallo: lower = 0.0296875, upper = 0.0310938
Intervallo: lower = 0.0303906, upper = 0.0310938
Intervallo: lower = 0.0307422, upper = 0.0310938
Intervallo: lower = 0.030918, upper = 0.0310938
Intervallo: lower = 0.030918, upper = 0.0310059
Intervallo: lower = 0.0309619, upper = 0.0310059
Intervallo: lower = 0.0309839, upper = 0.0310059
Intervallo: lower = 0.0309949, upper = 0.0310059
Intervallo: lower = 0.0310004, upper = 0.0310059
Intervallo: lower = 0.0310004, upper = 0.0310031
Intervallo: lower = 0.0310017, upper = 0.0310031
print(sol)
[1] 0.03100243

Metodo delle secanti

Trova il coefficiente di attrito usando il metodo delle secanti

sol <- secant_method(colebrook, x0 = 0.02, x1 = 0.03)
Iterazione 1: x = 0.030725
Iterazione 2: x = 0.0309953
Iterazione 3: x = 0.031002
Iterazione 4: x = 0.031002
print(sol)
[1] 0.03100205

Metodo di Newton

Definizione della derivata della funzione di Colebrook rispetto a f

colebrook_prime <- function(f) {
  epsilon_D <- 0.005
  Re <- 1.5e5
  num <- 2 * 2.51 / (Re * f^(1.5) * log(10))
  den <- epsilon_D / 3.7 + 2.51 / (Re * sqrt(f))
  return(-0.5 * f^(-1.5) - num / den)
}

Trova il coefficiente di attrito usando il metodo di Newton-Raphson

sol <- newton_method(colebrook, colebrook_prime, initial_guess = 0.02)
Iterazione 1: x = 0.0277966
Iterazione 2: x = 0.0307201
Iterazione 3: x = 0.0309974
Iterazione 4: x = 0.031002
Iterazione 5: x = 0.031002
print(sol)
[1] 0.03100205

3.4 Equazione di Van der Waals (Equilibrio chimico)

L’equazione di Van der Waals è data da:

\left( P + \dfrac{a}{V^2} \right) (V - b) = RT

Dove:

  • P = 100\, \text{kPa}
  • T = 300\, \text{K}
  • a = 0.3640\, \text{Pa} \cdot \text{m}^6 / \text{mol}^2 (anidride carbonica)
  • b = 4.267\cdot 10^{-5}\, \text{m}^3 / \text{mol} (anidride carbonica)

Trova il volume molare V. A questa pressione il gas è quasi ideale e ci aspettiamo V\approx RT/P\approx 0.025\,\text{m}^3/\text{mol}.

ConsiglioSoluzione in R

Definizione della funzione di Van der Waals

vanderwaals <- function(V) {
  P <- 100e3     # kPa to Pa
  R <- 8.314     # J/(mol K)
  T <- 300       # K
  a <- 0.3640    # Pa m^6/mol^2 (CO2)
  b <- 4.267e-5  # m^3/mol (CO2)
  return((P + a/(V^2)) * (V - b) - R * T)
}

Metodo di Bisezione

Trova il volume usando il metodo di bisezione

sol <- bisection_method(vanderwaals, 0.01, 0.1)
Intervallo: lower = 0.01, upper = 0.1
Intervallo: lower = 0.01, upper = 0.055
Intervallo: lower = 0.01, upper = 0.0325
Intervallo: lower = 0.02125, upper = 0.0325
Intervallo: lower = 0.02125, upper = 0.026875
Intervallo: lower = 0.0240625, upper = 0.026875
Intervallo: lower = 0.0240625, upper = 0.0254688
Intervallo: lower = 0.0247656, upper = 0.0254688
Intervallo: lower = 0.0247656, upper = 0.0251172
Intervallo: lower = 0.0247656, upper = 0.0249414
Intervallo: lower = 0.0247656, upper = 0.0248535
Intervallo: lower = 0.0248096, upper = 0.0248535
Intervallo: lower = 0.0248315, upper = 0.0248535
Intervallo: lower = 0.0248315, upper = 0.0248425
Intervallo: lower = 0.024837, upper = 0.0248425
Intervallo: lower = 0.024837, upper = 0.0248398
Intervallo: lower = 0.024837, upper = 0.0248384
print(sol)
[1] 0.02483772

Metodo delle secanti

Trova il volume usando il metodo delle secanti

sol <- secant_method(vanderwaals, x0 = 0.02, x1 = 0.03)
Iterazione 1: x = 0.0248323
Iterazione 2: x = 0.0248384
Iterazione 3: x = 0.0248384
print(sol)
[1] 0.02483837

Metodo di Newton

Derivata della funzione di Van Der Waals

vanderwaals_derivative <- function(V) {
  P <- 100e3     # Pa
  R <- 8.314     # J/(mol K)
  T <- 300       # K
  a <- 0.3640    # Pa m^6/mol^2 (CO2)
  b <- 4.267e-5  # m^3/mol (CO2)

  # Calcolo della derivata
  term1 <- P + a / (V^2)
  term2 <- (2 * a * (V - b)) / (V^3)

  return(term1 - term2)
}

Trova il volume usando il metodo di Newton

sol <- newton_method(vanderwaals, vanderwaals_derivative, initial_guess = 0.02)
Iterazione 1: x = 0.024847
Iterazione 2: x = 0.0248384
Iterazione 3: x = 0.0248384
print(sol)
[1] 0.02483837

3.5 Equazione di Shockley per i diodi

La corrente che attraversa un diodo è descritta dall’equazione di Shockley: I = I_0 \left( e^{\dfrac{V}{nV_T}} - 1 \right) Dove: - I = 2 \, \text{mA} (corrente attraverso il diodo) - I_0 = 10^{-12} \, \text{A} (corrente di saturazione inversa) - n = 1 (fattore di idealità del diodo) - V_T = 26 \, \text{mV} (tensione termica)

Trova la tensione V del diodo.

ConsiglioSoluzione in R
# Definizione della funzione di Shockley
shockley <- function(V) {
  I <- 2e-3  # Corrente attraverso il diodo in Ampere
  I0 <- 1e-12  # Corrente di saturazione inversa in Ampere
  n <- 1  # Fattore di idealità del diodo
  VT <- 26e-3  # Tensione termica in Volt
  return(I0 * (exp(V / (n * VT)) - 1) - I)
}

Metodo di Bisezione

Trova la tensione del diodo usando il metodo di bisezione

sol <- bisection_method(shockley, 0, 1)
Intervallo: lower = 0, upper = 1
Intervallo: lower = 0.5, upper = 1
Intervallo: lower = 0.5, upper = 0.75
Intervallo: lower = 0.5, upper = 0.625
Intervallo: lower = 0.5, upper = 0.5625
Intervallo: lower = 0.53125, upper = 0.5625
Intervallo: lower = 0.546875, upper = 0.5625
Intervallo: lower = 0.554688, upper = 0.5625
Intervallo: lower = 0.554688, upper = 0.558594
Intervallo: lower = 0.556641, upper = 0.558594
Intervallo: lower = 0.556641, upper = 0.557617
Intervallo: lower = 0.556641, upper = 0.557129
Intervallo: lower = 0.556641, upper = 0.556885
Intervallo: lower = 0.556763, upper = 0.556885
Intervallo: lower = 0.556824, upper = 0.556885
Intervallo: lower = 0.556824, upper = 0.556854
Intervallo: lower = 0.556824, upper = 0.556839
Intervallo: lower = 0.556824, upper = 0.556831
Intervallo: lower = 0.556824, upper = 0.556828
Intervallo: lower = 0.556826, upper = 0.556828
print(sol)
[1] 0.5568266

Metodo delle secanti

Trova la tensione del diodo usando il metodo delle secanti

sol <- secant_method(shockley, x0 = 0.6, x1 = 0.7)
Iterazione 1: x = 0.598232
Iterazione 2: x = 0.596581
Iterazione 3: x = 0.576856
Iterazione 4: x = 0.567524
Iterazione 5: x = 0.560234
Iterazione 6: x = 0.557467
Iterazione 7: x = 0.556868
Iterazione 8: x = 0.556827
Iterazione 9: x = 0.556827
print(sol)
[1] 0.5568267

Metodo di Newton

Derivata della funzione di Shockley

shockley_derivative <- function(V) {
  I0 <- 1e-12  # Corrente di saturazione inversa in Ampere
  n <- 1       # Fattore di idealità del diodo
  VT <- 26e-3  # Tensione termica in Volt
  exp_term <- exp(V / (n * VT))  # Calcola l'esponenziale
  derivative <- (I0 / (n * VT)) * exp_term  # Calcola la derivata
  return(derivative)
}

Trova la tensione del diodo usando il metodo di Newton

sol <- newton_method(shockley, shockley_derivative, initial_guess = 0.6)
Iterazione 1: x = 0.578941
Iterazione 2: x = 0.564048
Iterazione 3: x = 0.557743
Iterazione 4: x = 0.556843
Iterazione 5: x = 0.556827
Iterazione 6: x = 0.556827
print(sol)
[1] 0.5568267

3.6 Problemi difficili: quando serve la salvaguardia

Gli esempi precedenti sono gentili: la funzione è monotona vicino alla radice, la derivata non si annulla, un punto iniziale ragionevole basta. In questi casi bisezione, secanti e Newton arrivano tutti in fondo e la scelta del metodo è una questione di efficienza.

I sette problemi che seguono sono di natura diversa. Vengono da applicazioni reali o da banchi di prova pubblicati, e su ciascuno almeno uno dei metodi puri fallisce: diverge, converge a una radice sbagliata, va in overflow, oppure striscia così lentamente da essere inutilizzabile. Sono gli esempi che giustificano i metodi ibridi di Metodi ibridi e algoritmi con salvaguardia.

Un solutore ibrido minimo

Per i confronti useremo due strumenti. Il primo è uniroot, il solutore di libreria di R, che implementa l’algoritmo di Brent–Dekker (Metodo di Brent–Dekker): accetta un intervallo con cambio di segno e restituisce anche il numero di valutazioni usate. Il secondo è una versione compatta di Newton con salvaguardia (Gli ibridi con derivate: uno schema comune), che rifiuta il passo di Newton quando esce dal bracket o non dimezza l’incremento precedente:

newton_sicuro <- function(f, df, a, b, tol = 1e-12, nmax = 100) {
  fa <- f(a); fb <- f(b)
  if (fa*fb > 0) stop("gli estremi non racchiudono una radice")
  if (fa < 0) { xl <- a; xh <- b } else { xl <- b; xh <- a }
  x <- 0.5*(a+b); dxold <- abs(b-a); dx <- dxold
  fx <- f(x); dfx <- df(x); if (!is.finite(dfx)) dfx <- 0
  storia <- data.frame()
  for (k in 1:nmax) {
    fuori <- ((x-xh)*dfx - fx) * ((x-xl)*dfx - fx) > 0
    lento <- abs(2*fx) > abs(dxold*dfx)
    if (fuori || lento) { dxold <- dx; dx <- 0.5*(xh-xl); x <- xl + dx; tipo <- "bisezione" }
    else                { dxold <- dx; dx <- fx/dfx;      x <- x - dx;  tipo <- "Newton"    }
    fx <- f(x); dfx <- df(x); if (!is.finite(dfx)) dfx <- 0
    if (fx < 0) xl <- x else xh <- x
    storia <- rbind(storia, data.frame(k = k, passo = tipo, x = x, fx = fx))
    if (abs(dx) < tol || fx == 0) break
  }
  storia
}

3.7 Equazione di Keplero: orbite quasi paraboliche

Determinare dove si trova un corpo celeste sulla sua orbita a un dato istante significa risolvere l’equazione di Keplero

f(E) = E - e\sin E - M = 0 ,

dove M è l’anomalia media (proporzionale al tempo trascorso dal perielio), e l’eccentricità dell’orbita ed E l’anomalia eccentrica incognita. È il calcolo che sta dietro a ogni effemeride e a ogni propagatore orbitale; la sua soluzione numerica è un problema con quattro secoli di storia [2].

Per orbite quasi circolari (e\ll1) la funzione è quasi lineare e qualunque metodo funziona. Il caso difficile è quello quasi parabolico, e\to1, tipico delle comete: in E=0 la derivata

f'(E) = 1 - e\cos E

vale 1-e, cioè quasi zero. Il passo di Newton calcolato dalla scelta naturale E_0=M viene diviso per questa quantità minuscola e scaglia l’iterata lontanissimo, fuori dall’intervallo fisicamente ammissibile [0,\pi].

e <- 0.999; M <- 0.01
fK  <- function(E) E - e*sin(E) - M
dfK <- function(E) 1 - e*cos(E)

newton_puro <- function(f, df, x0, n) {
  x <- x0; tr <- x
  for (k in 1:n) {
    d <- df(x); if (!is.finite(d) || d == 0) break
    x <- x - f(x)/d; tr <- c(tr, x)
    if (!is.finite(x)) break
  }
  tr
}
signif(newton_puro(fK, dfK, M, 12), 7)
 [1]   0.01000000   9.52458400   4.70312600  -0.93676740  -0.58907810
 [6]  -0.32907450  -0.03183997   6.63146200 -96.36529000 -33.49573000
[11] -11.54740000  14.50457000   4.52375900

La prima iterata salta a 9.52, ben fuori dall’intervallo fisico, poi il metodo vaga fra -96 e +14 prima di ricadere per puro caso vicino alla radice dopo ventidue passi. Con e=0.9999 e M=0.1 non ci ricade affatto: le iterate raggiungono 1.2\cdot10^{10} e dopo trenta passi il metodo è a 3.8\cdot10^{6}, cioè non converge.

Il bracket, invece, esiste sempre ed è noto a priori senza alcun calcolo: poiché \left\lvert e\sin E\right\rvert\le1, per M\in[0,\pi] la radice sta certamente in [0,\pi].

u <- uniroot(fK, c(0, pi), tol = 1e-15)
cat(sprintf("Brent: E = %.15f   f(E) = %.2e   valutazioni = %d\n",
            u$root, u$f.root, u$iter))
Brent: E = 0.387461123237761   f(E) = 8.67e-18   valutazioni = 14
knitr::kable(newton_sicuro(fK, dfK, 0, pi),
             digits = c(0, 0, 12, 20), align = "r",
             caption = "Keplero con $e=0.999$, $M=0.01$: Newton con salvaguardia")
Keplero con e=0.999, M=0.01: Newton con salvaguardia
k passo x fx
1 Newton 1.0090000 1.535467e-01
2 Newton 0.6807862 4.201146e-02
3 Newton 0.4929827 1.020043e-02
4 Newton 0.4079477 1.618147e-03
5 Newton 0.3884475 7.421816e-05
6 Newton 0.3874636 1.830228e-07
7 Newton 0.3874611 1.122333e-12
8 Newton 0.3874611 -4.684000e-17
9 Newton 0.3874611 8.670000e-18

Vale la pena osservare che nella colonna passo non compare nessuna bisezione: tutti i passi di Newton vengono accettati. Il guadagno non sta dunque nell’aver sostituito Newton con qualcosa di più lento, ma in due effetti del bracket. Il primo è che il metodo parte dal punto medio dell’intervallo, \pi/2, dove f'=1-e\cos(\pi/2)=1 e il modello lineare è eccellente, invece che dal punto E_0=M suggerito dalla fisica, dove f'\approx1-e è quasi nullo. Il secondo è che il test di ammissibilità impedisce comunque a qualunque passo di uscire da [0,\pi]. La bisezione resta disponibile come assicurazione e in questo caso non serve mai: la salvaguardia lavora prevenendo il disastro, non riparandolo.

3.8 Frequenze proprie di una trave a sbalzo

Le frequenze di vibrazione flessionale di una trave incastrata a un estremo e libera all’altro si ottengono dalle radici dell’equazione delle frequenze

g(x) = \cos x \cosh x + 1 = 0, \qquad x = \beta L ,

e la k-esima frequenza propria vale \omega_k = (\beta_k L)^2\sqrt{EJ/(\rho A L^4)}. È un calcolo di routine in progettazione strutturale e in analisi modale.

Il problema numerico non sta nella matematica ma nella scala. Il fattore \cosh x cresce come e^{x}/2, quindi g assume valori enormi non appena ci si allontana dall’origine:

g <- function(x) cos(x)*cosh(x) + 1
cat(sprintf("g(20) = %.3e\ng(40) = %.3e\ng(100) = %.3e\ng(800) = %.3e\n",
            g(20), g(40), g(100), g(800)))
g(20) = 9.899e+07
g(40) = -7.849e+16
g(100) = 1.159e+43
g(800) = -Inf
cat("prodotto g(1)*g(40) =", g(1)*g(40), "\n")
prodotto g(1)*g(40) = -1.439362e+17 

Tre conseguenze pratiche, tutte già incontrate in Metodo di bisezione (o dicotomico) e Criteri di arresto e accuratezza raggiungibile:

  • a x\approx710 il \cosh va in overflow e g restituisce Inf o NaN;
  • il prodotto g(a)g(b) usato ingenuamente per il test di cambio di segno vale già -1.4\cdot10^{17} per le prime radici, e va in overflow molto prima della funzione stessa: è il motivo per cui il test va fatto confrontando i segni;
  • un criterio d’arresto sul residuo \left\lvert g\right\rvert è privo di senso, perché la scala di g cambia di venti ordini di grandezza lungo l’intervallo.

Il rimedio corretto non è numerico ma algebrico: si divide per \cosh x, ottenendo una funzione equivalente, limitata e ben condizionata,

\tilde g(x) = \cos x + \frac{1}{\cosh x} = 0 .

Le radici sono le stesse, ma \tilde g oscilla in [-1,1] e tende a \cos x: le radici si addensano attorno a (2k-1)\pi/2, il che fornisce gratuitamente un bracket per ciascuna.

g_scal <- function(x) cos(x) + 1/cosh(x)
radici <- sapply(1:6, function(k) {
  centro <- (2*k-1)*pi/2
  uniroot(g_scal, c(centro - 1.4, centro + 1.4), tol = 1e-14)$root
})
knitr::kable(
  data.frame(k = 1:6,
             `$\\beta_k L$` = sprintf("%.10f", radici),
             `$(2k-1)\\pi/2$` = sprintf("%.10f", (2*(1:6)-1)*pi/2),
             `$(\\beta_k L)^2$` = sprintf("%.6f", radici^2),
             check.names = FALSE),
  align = "r", caption = "Prime sei radici dell'equazione delle frequenze")
Prime sei radici dell’equazione delle frequenze
k \beta_k L (2k-1)\pi/2 (\beta_k L)^2
1 1.8751040687 1.5707963268 3.516015
2 4.6940911330 4.7123889804 22.034492
3 7.8547574382 7.8539816340 61.697214
4 10.9955407349 10.9955742876 120.901916
5 14.1371683910 14.1371669412 199.859530
6 17.2787595321 17.2787595947 298.555531

Il primo valore, \beta_1L = 1.8751040687, è la costante tabulata in tutti i manuali di vibrazioni meccaniche. Si noti come dalla terza radice in poi \beta_kL coincida con (2k-1)\pi/2 fino a quasi tutte le cifre: il termine 1/\cosh x diventa trascurabile e le radici si allineano agli zeri del coseno.

3.9 Profondità di immersione di una sfera

Una sfera omogenea di raggio R e densità relativa s galleggia immersa fino alla profondità h. Uguagliando il peso alla spinta di Archimede — cioè il volume della calotta immersa, \pi h^2(3R-h)/3, al volume spostato s\,\tfrac43\pi R^3 — si ottiene

f(h) = h^3 - 3Rh^2 + 4sR^3 = 0 .

È un classico dei testi di metodi numerici per l’ingegneria, e la sua difficoltà non è la convergenza ma quale radice si trova. Il polinomio è cubico e per 0<s<1 ha tre radici reali, di cui una sola ha senso fisico: quella in [0,2R].

R <- 1; s <- 0.6
fh  <- function(h) h^3 - 3*R*h^2 + 4*s*R^3
dfh <- function(h) 3*h^2 - 6*R*h
cat("tutte le radici reali:", signif(sort(Re(polyroot(c(4*s*R^3, 0, -3*R, 1)))), 10), "\n")
tutte le radici reali: -0.7952197 1.134138 2.661082 
cat("intervallo fisicamente ammissibile: [0,", 2*R, "]\n\n")
intervallo fisicamente ammissibile: [0, 2 ]
cat("Newton da h0 = 2.5 :", signif(tail(newton_puro(fh, dfh, 2.5, 10), 1), 10), "<- NON fisica\n")
Newton da h0 = 2.5 : 2.661082 <- NON fisica
cat("Newton da h0 = 1.0 :", signif(tail(newton_puro(fh, dfh, 1.0, 10), 1), 10), "<- corretta\n")
Newton da h0 = 1.0 : 1.134138 <- corretta

Partendo da h_0=2.5 il metodo di Newton converge perfettamente — a h=2.661, che è maggiore del diametro: la sfera sarebbe immersa più del suo stesso ingombro. Il risultato è matematicamente ineccepibile e fisicamente assurdo, e nessun criterio d’arresto può accorgersene.

Un metodo con bracket, invece, riceve l’informazione fisica come dato di ingresso e non può restituire una radice fuori da [0,2R]:

u <- uniroot(fh, c(0, 2*R), tol = 1e-14)
cat(sprintf("Brent su [0, 2R]: h = %.12f  (%d valutazioni)\n", u$root, u$iter))
Brent su [0, 2R]: h = 1.134137845705  (6 valutazioni)

È la lezione più importante di questo capitolo: il bracket non è soltanto una garanzia di convergenza, è il modo in cui si comunica al solutore ciò che si sa del problema.

3.10 Catenaria: la freccia di una linea aerea

Un cavo omogeneo sospeso fra due sostegni alla stessa quota assume la forma di una catenaria. Detta 2b la campata e d la freccia (l’abbassamento in mezzeria), il parametro a della catenaria risolve

f(a) = a\left(\cosh\frac{b}{a}-1\right) - d = 0 ,

ed è la quantità da cui si ricavano tiro e tensione del conduttore. Il problema è reale e quotidiano nel progetto delle linee elettriche.

La difficoltà è duplice e di natura puramente numerica. Primo, il condizionamento: per frecce piccole a diventa grande e, sviluppando, f(a)\approx b^2/(2a)-d, da cui f'(a)\approx-b^2/(2a^2), che tende a zero come 1/a^2.

b <- 50
f_cat <- function(a, d) 2*a*sinh(b/(2*a))^2 - d       # forma stabile, si veda oltre
tab <- t(sapply(c(10, 1, 0.1, 0.01), function(d) {
  a  <- uniroot(function(a) f_cat(a, d), c(10, 1e7), tol = 1e-12)$root
  dp <- (f_cat(a + 1e-3, d) - f_cat(a - 1e-3, d))/2e-3
  c(d = d, a = a, derivata = dp, condizionamento = 1/abs(dp))
}))
knitr::kable(tab, digits = c(3, 4, 9, 1), align = "r",
             caption = "Condizionamento della radice al diminuire della freccia")
Condizionamento della radice al diminuire della freccia
d a derivata condizionamento
10.00 126.6324 -0.081015286 12.3
1.00 1250.1666 -0.000800107 1249.8
0.10 12500.0167 -0.000008000 124999.8
0.01 125000.0017 -0.000000080 12499999.5

Passando da una freccia di 10 m a una di 1 cm il numero di condizionamento 1/\left\lvert f'(\alpha)\right\rvert peggiora di sei ordini di grandezza: per la Intervallo di incertezza e condizionamento della radice l’incertezza sul parametro a cresce nella stessa misura, qualunque metodo si usi.

Secondo, la cancellazione. Scrivere \cosh(x)-1 per x=b/a piccolo è il prototipo della sottrazione catastrofica, perché si sottrae 1 da un numero che vale 1+x^2/2. L’identità \cosh x-1=2\sinh^2(x/2) elimina il problema:

xs <- c(1e-2, 1e-3, 1e-4, 1e-5)
confronto <- data.frame(
  `$x$`                 = sprintf("%.0e", xs),
  `$\\cosh x-1$`        = sprintf("%.16e", cosh(xs)-1),
  `$2\\sinh^2(x/2)$`    = sprintf("%.16e", 2*sinh(xs/2)^2),
  `cifre corrette`      = sprintf("%.1f",
      -log10(pmax(abs((cosh(xs)-1) - 2*sinh(xs/2)^2)/(2*sinh(xs/2)^2), 1e-17))),
  check.names = FALSE)
knitr::kable(confronto, align = "r",
             caption = "Cancellazione nella valutazione di $\\cosh x-1$")
Cancellazione nella valutazione di \cosh x-1
x \cosh x-1 2\sinh^2(x/2) cifre corrette
1e-02 5.0000416668138925e-05 5.0000416668055563e-05 11.8
1e-03 5.0000004159223010e-07 5.0000004166666821e-07 9.8
1e-04 4.9999999696126451e-09 5.0000000041666668e-09 8.2
1e-05 5.0000004137018550e-11 5.0000000000416681e-11 7.1

A x=10^{-5} la forma ingenua ha perso nove cifre significative su sedici. Nessun solutore può recuperare quell’informazione: la funzione va riscritta prima di chiamare il solutore, non dopo.

3.11 Una funzione piatta: il problema 13 del banco TOMS 748

Il banco di prova usato da Alefeld, Potra e Shi per validare l’algoritmo 748 [1] contiene funzioni costruite apposta per mettere in crisi i metodi di ricerca di zeri. La più istruttiva è

f(x)=\begin{cases} x\,e^{-1/x^2}, & x\neq0,\\ 0, & x=0,\end{cases} \qquad x\in[-1,4].

È una funzione C^\infty con un unico zero in x=0, dove però tutte le derivate si annullano. Il risultato è che in aritmetica finita la funzione non è semplicemente piccola vicino allo zero: è esattamente zero.

f13 <- function(x) ifelse(x == 0, 0, x*exp(-1/x^2))
knitr::kable(
  data.frame(x = c(0.2, 0.1, 0.06, 0.05, 0.04, 0.037, 0.03, 0.02),
             `$f(x)$` = sprintf("%.3e", f13(c(0.2,0.1,0.06,0.05,0.04,0.037,0.03,0.02))),
             check.names = FALSE),
  align = "r", caption = "Underflow di $x\\,e^{-1/x^2}$ in prossimità dello zero")
Underflow di x\,e^{-1/x^2} in prossimità dello zero
x f(x)
0.200 2.778e-12
0.100 3.720e-45
0.060 1.383e-122
0.050 9.576e-176
0.040 1.472e-273
0.037 2.155e-319
0.030 0.000e+00
0.020 0.000e+00

Sotto \left\lvert x\right\rvert\approx0.037 l’esponenziale va in underflow e f restituisce zero esatto: ogni punto dell’intervallo (-0.037,\,0.037) è, per il calcolatore, una radice. L’intervallo di incertezza della Intervallo di incertezza e condizionamento della radice qui non è dell’ordine di 10^{-8} o 10^{-5}, ma di 10^{-2}.

Per il metodo di Newton il disastro è di altro tipo. Si ha f/f' = x^3/(x^2+2), quindi lo schema è x_{k+1}\approx x_k - x_k^3/2: per x piccolo l’incremento è cubicamente piccolo e il metodo striscia.

df13 <- function(x) if (x == 0) 0 else exp(-1/x^2)*(1 + 2/x^2)
tr <- newton_puro(f13, Vectorize(df13), 1, 25)
cat("Newton da x0 = 1, iterate 18--25:\n"); print(signif(tail(tr, 8), 6))
Newton da x0 = 1, iterate 18--25:
[1] 0.224612 0.219086 0.213951 0.209164 0.204686 0.200486 0.196537 0.192813
u <- uniroot(f13, c(-1, 4), tol = 1e-14)
cat(sprintf("\nBrent su [-1,4]: x = %.3e  f(x) = %g  (%d valutazioni)\n",
            u$root, u$f.root, u$iter))

Brent su [-1,4]: x = -1.906e-02  f(x) = -0  (14 valutazioni)

Dopo venticinque iterazioni Newton è ancora a x\approx0.19, e procede guadagnando una frazione di cifra per passo; un metodo con bracket chiude la partita in quattordici valutazioni, semplicemente perché dimezza l’intervallo senza chiedersi quanto la funzione sia piatta.

3.12 Il problema 10 del banco TOMS 748

Sempre dallo stesso banco [1], con un difetto opposto al precedente:

f(x) = e^{-nx}(x-1) + x^{n} = 0, \qquad x\in[0,1].

Per n grande la funzione è piatta e quasi costante (\approx-1) su quasi tutto l’intervallo, poi risale bruscamente verso +1 in un intorno sempre più stretto della radice. In x=0 e in x=1 la derivata non dà alcuna informazione utile sulla posizione dello zero.

n <- 20
f10  <- function(x) exp(-n*x)*(x-1) + x^n
df10 <- function(x) exp(-n*x)*(1 - n*(x-1)) + n*x^(n-1)
cat("Newton da x0 = 0:\n"); print(signif(head(newton_puro(f10, df10, 0, 9), 10), 6))
Newton da x0 = 0:
 [1] 0.000000 0.047619 0.095125 0.142507 0.189752 0.236846 0.283771 0.330509
 [9] 0.377034 0.423317
cat("Newton da x0 = 1:\n"); print(signif(head(newton_puro(f10, df10, 1, 9), 10), 6))
Newton da x0 = 1:
 [1] 1.000000 0.950000 0.902500 0.857375 0.814506 0.773781 0.735093 0.698341
 [9] 0.663446 0.630398

Da entrambi gli estremi Newton avanza a passi quasi costanti — da sinistra +0.048 per volta, da destra -5\% per volta — comportandosi come un metodo lineare lentissimo: in nove iterazioni, cioè diciotto valutazioni fra funzione e derivata, non ha ancora raggiunto la radice.

righe <- lapply(c(1, 5, 10, 20), function(n) {
  f <- function(x) exp(-n*x)*(x-1) + x^n
  u <- uniroot(f, c(0, 1), tol = 1e-14)
  data.frame(n = n, radice = sprintf("%.12f", u$root), valutazioni = u$iter)
})
knitr::kable(do.call(rbind, righe), align = "r",
             caption = "Brent sul problema 10 di TOMS 748")
Brent sul problema 10 di TOMS 748
n radice valutazioni
1 0.401058137542 7
5 0.516153518758 10
10 0.539522226908 12
20 0.552704666678 13

Brent chiude con un numero di valutazioni che cresce appena con n, e con la garanzia di non uscire mai da [0,1].

3.13 Tassi interni di rendimento multipli

Il tasso interno di rendimento (TIR, o IRR) di un progetto è il tasso r che annulla il valore attuale netto dei flussi di cassa C_0,C_1,\dots,C_n:

\mathrm{VAN}(r)=\sum_{t=0}^{n}\frac{C_t}{(1+r)^t}=0 .

Se i flussi cambiano segno una sola volta — un esborso iniziale seguito da ricavi — la funzione è monotona e la radice è unica. Ma per i progetti con flussi non convenzionali, in cui un esborso rilevante ricompare alla fine (lo smantellamento di un impianto, la bonifica di un sito, il ripristino di una cava), i cambi di segno sono due e di radici ce ne sono due. È il problema classico del razionamento del capitale segnalato da Lorie e Savage [3].

flussi <- c(-1000, 2500, -1540)
van  <- function(r) sum(flussi / (1+r)^(0:2))
dvan <- function(r) sum(-(0:2) * flussi / (1+r)^(1:3))
van  <- Vectorize(van); dvan <- Vectorize(dvan)

cat("radici esatte:", Re(polyroot(rev(flussi)))-1, "\n")
radici esatte: 0.1 0.4 
cat(sprintf("VAN(0%%) = %.1f   VAN(20%%) = %.1f   VAN(100%%) = %.1f\n",
            van(0), van(0.2), van(1)))
VAN(0%) = -40.0   VAN(20%) = 13.9   VAN(100%) = -135.0
cat("\nNewton da r0 =  5% :", sprintf("%.4f", tail(newton_puro(van, dvan, 0.05, 20), 1)), "\n")

Newton da r0 =  5% : 0.1000 
cat(  "Newton da r0 = 50% :", sprintf("%.4f", tail(newton_puro(van, dvan, 0.50, 20), 1)), "\n")
Newton da r0 = 50% : 0.4000 

Il progetto ha due tassi interni di rendimento, 10\% e 40\%, entrambi matematicamente legittimi. Newton restituisce l’uno o l’altro a seconda del punto iniziale, e non ha modo di segnalare che esiste una seconda soluzione: la risposta dipende da un dettaglio che nulla ha a che vedere con l’economia del progetto.

cat(sprintf("Brent su [0%%, 20%%] : r = %.6f\n", uniroot(van, c(0, 0.2), tol = 1e-14)$root))
Brent su [0%, 20%] : r = 0.100000
cat(sprintf("Brent su [20%%, 100%%]: r = %.6f\n", uniroot(van, c(0.2, 1), tol = 1e-14)$root))
Brent su [20%, 100%]: r = 0.400000

Separando i due intervalli si ottengono entrambe le radici, ciascuna con la garanzia di esistenza che il cambio di segno fornisce. La morale finanziaria — che il TIR non è un criterio affidabile per i flussi non convenzionali — è a sua volta una conseguenza diretta di un fatto numerico.

3.14 Tre patologie del metodo di Newton

Negli esempi precedenti il metodo di Newton fallisce per colpa del problema: una derivata quasi nulla, una seconda radice, un intervallo in cui la funzione è numericamente zero. I tre che seguono sono di natura diversa e più istruttiva: la funzione è regolare quanto si vuole, ha una sola radice semplice o una sola radice e basta, eppure Newton non converge — e non per sfortuna, ma per struttura.

Servono a fissare un punto che il Convergenza locale enuncia e che è facile dimenticare: la convergenza di Newton è locale. Il teorema garantisce un intorno della radice in cui il metodo converge, non dice quanto sia grande, e fuori da quell’intorno non promette nulla. Sono la versione dettagliata dei modi di fallimento elencati in Quando Newton fallisce.

Arcotangente: converge o diverge secondo il punto iniziale

Consideriamo

f(x) = \arctan x ,

che ha l’unica radice \alpha=0, è analitica su tutto \mathbb{R}, strettamente crescente, con f'(0)=1: tutte le ipotesi di convergenza locale sono soddisfatte nel modo più comodo possibile. L’iterazione di Newton è

N(x) = x - \dfrac{\arctan x}{1/(1+x^2)} = x - (1+x^2)\arctan x . \tag{3.1}

Il guaio si legge nella 3.1: il fattore 1+x^2 cresce come x^2, mentre \arctan x tende a \pi/2. Per x grande il passo di Newton vale circa -\tfrac{\pi}{2}x^2, cioè è enormemente più lungo della distanza dalla radice. La tangente è quasi orizzontale e interseca l’asse molto lontano, dalla parte opposta.

Esiste quindi una soglia x^\star che separa i due comportamenti. Per simmetria (f è dispari, quindi N è dispari) la soglia è il punto in cui N(x^\star)=-x^\star, cioè la radice di

2x = (1+x^2)\arctan x . \tag{3.2}

f_at  <- function(x) atan(x)
df_at <- function(x) 1/(1+x^2)

xstar <- uniroot(function(x) 2*x - (1+x^2)*atan(x), c(1, 2), tol = 1e-15)$root
cat(sprintf("soglia x* = %.15f\n", xstar))
soglia x* = 1.391745200270735

Il comportamento è allora completamente determinato dal punto iniziale:

  • se |x_0|<x^\star il metodo converge, e converge quadraticamente;
  • se |x_0|=x^\star le iterate cadono nel ciclo x^\star \to -x^\star \to x^\star \to \cdots;
  • se |x_0|>x^\star le iterate si allontanano, cambiando segno a ogni passo, e divergono.
x0 <- c(1.0, 1.35, 1.3918, 1.5)
tr <- lapply(x0, function(a) { t <- newton_puro(f_at, df_at, a, 8); length(t) <- 9; t })

tabella <- data.frame(k = 0:8)
for (i in seq_along(x0))
  tabella[[sprintf("$x_0=%.4f$", x0[i])]] <- formatC(tr[[i]], format = "g", digits = 6)

knitr::kable(tabella, align = "r", row.names = FALSE,
             caption = "Newton su $\\arctan x$ da quattro punti iniziali.")
Newton su \arctan x da quattro punti iniziali.
k x_0=1.0000 x_0=1.3500 x_0=1.3918 x_0=1.5000
0 1 1.35 1.3918 1.5
1 -0.570796 -1.28409 -1.39189 -1.69408
2 0.11686 1.12412 1.39213 2.32113
3 -0.00106102 -0.785872 -1.39275 -5.11409
4 7.9631e-10 0.291554 1.3944 32.2957
5 0 -0.016251 -1.39876 -1575.32
6 0 2.86105e-06 1.41033 3.89498e+06
7 0 -1.56129e-17 -1.44126 -2.38303e+13
8 0 0 1.52586 8.92028e+26

Le due colonne centrali sono le più istruttive. Con x_0=1.35, sotto la soglia, il metodo prima si allontana — arriva a -1.28, poi a 1.12 — e solo dopo quattro passi inverte la rotta e crolla sulla radice: la convergenza quadratica si vede soltanto alle ultime due righe. Con x_0=1.3918, che supera la soglia di appena 5\cdot10^{-5}, le iterate restano a lungo vicine a \pm1.39 e sembrano un ciclo, poi scivolano via e divergono. La differenza fra convergere e divergere sta nella quinta cifra decimale del punto iniziale, e nessuna informazione ricavabile da f nei punti visitati permette di prevederla.

AvvisoChe cosa insegna questo esempio

La funzione non ha difetti: è analitica, con una sola radice, semplice, e derivata mai nulla. Il fallimento è interamente dovuto al punto iniziale, e la frontiera fra i due regimi è una soglia netta che nessun test sui valori di f può individuare. Un metodo con salvaguardia (Metodi ibridi e algoritmi con salvaguardia) rende la cosa irrilevante: appena il passo di Newton esce dal bracket, viene sostituito da una bisezione.

Un ciclo che non finisce mai

Nell’esempio precedente il ciclo esisteva, ma per un solo valore del punto iniziale. Si può fare di peggio: costruire una funzione per cui Newton cicla da qualunque punto si parta. Basta

f(x) = \begin{cases} \sqrt{x} & x\geq 0, \\[0.3em] -\sqrt{-x} & x<0, \end{cases} \qquad\text{ovvero}\qquad f(x)=\operatorname{sign}(x)\sqrt{|x|} . \tag{3.3}

È continua, strettamente crescente, dispari, e ha l’unica radice \alpha=0. La derivata è

f'(x) = \dfrac{1}{2\sqrt{|x|}} ,

definita e positiva per ogni x\neq0. Il conto del passo di Newton si fa a mano:

N(x) = x - \dfrac{\operatorname{sign}(x)\sqrt{|x|}}{\dfrac{1}{2\sqrt{|x|}}} = x - 2\operatorname{sign}(x)\,|x| = x - 2x = -x . \tag{3.4}

L’iterazione è dunque esattamente x_{k+1}=-x_k: da qualunque x_0\neq0 si parta, le iterate rimbalzano fra x_0 e -x_0 per sempre, senza avvicinarsi di un millimetro alla radice che sta in mezzo.

f_sq  <- function(x) sign(x)*sqrt(abs(x))
df_sq <- function(x) 1/(2*sqrt(abs(x)))

for (a in c(1.3, -0.7, 1e6))
  cat(sprintf("%-6s", format(a)),
      sprintf("%9.3g", newton_puro(f_sq, df_sq, a, 6)), "\n")
1.3          1.3      -1.3       1.3      -1.3       1.3      -1.3       1.3 
-0.7        -0.7       0.7      -0.7       0.7      -0.7       0.7      -0.7 
1e+06      1e+06    -1e+06     1e+06    -1e+06     1e+06    -1e+06     1e+06 

Si noti l’ultima riga: anche partendo da 10^6 — cioè a un milione di distanza dalla radice — il metodo non diverge; semplicemente non si muove. E, cosa più insidiosa, non dà alcun segnale di errore: le iterate sono finite, la derivata è finita e non nulla, f è finita. Un criterio d’arresto basato su |x_{k+1}-x_k| non scatta mai, uno basato su |f(x_k)| nemmeno. Il programma gira fino al numero massimo di iterazioni e restituisce un numero che non è una radice.

L’origine del guaio è la tangente verticale nell’origine: f'(x)\to+\infty per x\to0. La radice è di «ordine frazionario» 1/2, e il Convergenza locale non si applica perché richiede f\in C^2 in un intorno della radice — qui nemmeno f' è limitata.

Fuga garantita

Cambiando l’esponente si passa dal ciclo alla divergenza. Sia

f(x) = \begin{cases} \sqrt[3]{x} & x\geq 0, \\[0.3em] -\sqrt[3]{-x} & x<0, \end{cases} \qquad\text{ovvero}\qquad f(x)=\operatorname{sign}(x)\sqrt[3]{|x|}, \tag{3.5}

che è la radice cubica reale, anch’essa continua, dispari, strettamente crescente, con l’unica radice \alpha=0. Ora

f'(x)=\dfrac{1}{3}|x|^{-2/3}, \qquad N(x) = x - \dfrac{\operatorname{sign}(x)|x|^{1/3}}{\tfrac13|x|^{-2/3}} = x - 3x = -2x . \tag{3.6}

L’iterazione è x_{k+1}=-2x_k: il modulo raddoppia a ogni passo e il segno si alterna. Non esiste alcun punto iniziale, per quanto vicino alla radice, da cui il metodo converga.

f_cb  <- function(x) sign(x)*abs(x)^(1/3)
df_cb <- function(x) (1/3)*abs(x)^(-2/3)

for (a in c(0.5, 1e-6))
  cat(sprintf("%-6s", format(a)),
      sprintf("%8.4g", newton_puro(f_cb, df_cb, a, 7)), "\n")
0.5         0.5       -1        2       -4        8      -16       32      -64 
1e-06     1e-06   -2e-06    4e-06   -8e-06  1.6e-05 -3.2e-05  6.4e-05 -0.000128 

La seconda riga è la più eloquente: partendo a 10^{-6} dalla radice, dopo venti iterazioni si è a distanza 1, e da lì si continua a raddoppiare. Qui il criterio d’arresto scatta — nella direzione sbagliata: |x_{k+1}-x_k| cresce, e un’implementazione che non controlli l’overflow finisce in Inf.

Un quadro unico

I due esempi non sono curiosità separate: appartengono alla stessa famiglia a un parametro

f_p(x) = \operatorname{sign}(x)\,|x|^{p}, \qquad p>0,

per la quale il passo di Newton si calcola una volta per tutte:

\dfrac{f_p(x)}{f_p'(x)} = \dfrac{\operatorname{sign}(x)|x|^{p}}{p\,|x|^{p-1}} = \dfrac{x}{p}, \qquad\text{quindi}\qquad \boxed{\;N_p(x)=\Bigl(1-\dfrac{1}{p}\Bigr)x. \;} \tag{3.7}

L’iterazione è lineare con fattore \lambda_p=1-1/p, e tutto dipende da |\lambda_p|:

p  <- c(1/4, 1/3, 1/2, 1, 2, 3)
tr <- lapply(p, function(q) {
  f  <- function(x) sign(x)*abs(x)^q
  df <- function(x) q*abs(x)^(q-1)
  formatC(newton_puro(f, df, 1, 5), format = "g", digits = 4)
})

knitr::kable(
  cbind(data.frame(`$p$`      = c("1/4","1/3","1/2","1","2","3"),
                   `$1-1/p$`  = formatC(1-1/p, format = "f", digits = 3),
                   check.names = FALSE),
        as.data.frame(do.call(rbind, tr))),
  col.names = c("$p$", "$1-1/p$", paste0("$x_", 0:5, "$")),
  align = "r", row.names = FALSE,
  caption = "Newton su $f_p(x)=\\operatorname{sign}(x)|x|^p$ a partire da $x_0=1$.")
Newton su f_p(x)=\operatorname{sign}(x)|x|^p a partire da x_0=1.
p 1-1/p x_0 x_1 x_2 x_3 x_4 x_5
1/4 -3.000 1 -3 9 -27 81 -243
1/3 -2.000 1 -2 4 -8 16 -32
1/2 -1.000 1 -1 1 -1 1 -1
1 0.000 1 0 0 0 0 0
2 0.500 1 0.5 0.25 0.125 0.0625 0.03125
3 0.667 1 0.6667 0.4444 0.2963 0.1975 0.1317

Le righe riassumono tutti i casi possibili.

p \lambda_p=1-1/p comportamento
p<1/2 \lambda_p<-1 diverge, oscillando, tanto più in fretta quanto più p è piccolo
p=1/3 -2 la fuga di Sezione 3.14.3
p=1/2 -1 il ciclo di Sezione 3.14.2
1/2<p<1 -1<\lambda_p<0 converge linearmente, oscillando
p=1 0 radice semplice: converge in un passo
p>1 0<\lambda_p<1 radice multipla di ordine p: converge linearmente con fattore 1-1/p

L’ultima riga è il risultato classico sulle radici multiple di Radici multiple e rallentamento della convergenza, che qui appare come un caso particolare. Le prime due sono il suo prolungamento naturale per esponenti minori di uno, dove la derivata esplode invece di annullarsi: la stessa formula, letta al di là della soglia p=1/2, smette di descrivere una convergenza lenta e descrive un ciclo o una fuga.

ConsiglioMorale

In tutti e tre gli esempi la funzione ha una sola radice e il metodo di Newton, applicato senza rete, produce una successione perfettamente finita e perfettamente inutile. Nessuno dei criteri d’arresto usuali se ne accorge. Il rimedio è sempre lo stesso: partire da un intervallo con cambio di segno e rifiutare il passo di Newton quando esce dal bracket — cioè uno qualsiasi dei metodi di Metodi ibridi e algoritmi con salvaguardia, che su tutti e tre questi problemi convergono senza sforzo.

cat(sprintf("arctan  su [-1, 5]        : %.3e\n", uniroot(f_at, c(-1, 5), tol = 1e-14)$root))
arctan  su [-1, 5]        : 2.877e-22
cat(sprintf("sign(x)sqrt(|x|) su [-2,1]: %.3e\n", uniroot(f_sq, c(-2, 1), tol = 1e-14)$root))
sign(x)sqrt(|x|) su [-2,1]: -1.957e-15
cat(sprintf("radice cubica su [-3, 2]  : %.3e\n", uniroot(f_cb, c(-3, 2), tol = 1e-14)$root))
radice cubica su [-3, 2]  : -2.988e-15

In sintesi

Ogni esempio illustra un modo diverso in cui un metodo puro può sbagliare, e il rimedio corrispondente.

Problema Che cosa va storto Rimedio
Keplero, e\to1 f'\approx1-e\approx0: Newton proietta l’iterata fuori da [0,\pi] bracket noto a priori + salvaguardia
Trave a sbalzo overflow di \cosh; il prodotto f(a)f(b) esplode riscalare la funzione; test sui segni
Sfera galleggiante converge a una radice non fisica il bracket codifica il vincolo fisico
Catenaria condizionamento \propto a^2; cancellazione in \cosh x-1 riformulare con 2\sinh^2(x/2); accettare il limite
TOMS 748 n.13 f\equiv0 in aritmetica finita su un intervallo ampio nessun metodo fa meglio: è il limite della Intervallo di incertezza e condizionamento della radice
TOMS 748 n.10 Newton avanza a passi costanti, come un metodo lineare bisezione garantita
TIR multiplo due radici: la risposta dipende dal punto iniziale un bracket per ciascuna radice
\arctan x Newton diverge se |x_0|>x^\star, pur essendo f analitica salvaguardia: il passo lungo viene rifiutato
\operatorname{sign}(x)\sqrt{|x|} ciclo x\mapsto-x da ogni punto iniziale il bracket si dimezza comunque
\operatorname{sign}(x)\sqrt[3]{|x|} fuga x\mapsto-2x da ogni punto iniziale il bracket si dimezza comunque

Il filo comune è che in nessuno di questi casi il problema sta nell’aritmetica del metodo: sta nel fatto che un metodo aperto non ha modo di sapere dove la radice deve stare. Il bracket è esattamente quell’informazione, ed è gratis — basta averla.


  1. Isaac Newton (1642-1727) e Joseph Raphson (1668 ca.-1715 ca.)↩︎