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.

Integrazione di equazioni differenziali ordinarie

1Introduzione

Per affrontare la complessità dei problemi che caratterizzano la fisica moderna esistono diverse strategie, distribuite lungo uno spettro continuo che unisce la formalizzazione puramente analitica alla risoluzione numerica di forza bruta. Molto spesso la ricerca si colloca in una posizione intermedia, adottando approcci teorico-computazionali ibridi: il modello fisico viene inizialmente semplificato attraverso opportune ipotesi teoriche, per poi validare la soluzione analitica tramite il calcolo numerico. Ma come si traduce, concretamente, un problema fisico in termini computazionali? Sebbene la risposta dipenda strettamente dalla natura del sistema in esame, la fisica computazionale ha sviluppato metodologie di carattere generale applicabili a vastissime classi di fenomeni, dalla meccanica quantistica all’astrofisica. In questa sezione, in particolare, analizzeremo i metodi per l’integrazione numerica di equazioni differenziali, uno strumento pilastro per l’indagine scientifica in ogni ambito della fisica contemporanea[1].

2Equazioni differenziali ordinarie

Moltissimi problemi di fisica si possono formalizzare in termini di equazioni differenziali, cioè relazioni che connettono una funzione incognita alle sue derivate. La maggior parte delle equazioni differenziali di interesse non possono essere risolte analiticamente, e richiedono quindi di essere affrontate con metodi numerici. In questo corso ci occuperemo principalmente delle cosiddette equazioni differenziali ordinarie (spesso chiamate ODE, per ordinary differential equations), in cui la funzione incognita è di una sola variabile.

Un’equazione differenziale si dice dell’ordine nn-esimo se contiene al suo interno derivate nn-esime della funzione incognita. La soluzione generale di una ODE di ordine nn-esimo contiene nn costanti di integrazione indipendenti il cui valore viene fissato specificando nn condizioni (dette solitamente condizioni iniziali o al bordo) per ottenere una soluzione particolare.

Nel seguito ci occuperemo principalmente di metodi per la risoluzione numerica di ODE del primo ordine, cioè del tipo

x=f(x,t),x' = f(x, t),

dove x=x(t)x = x(t) è la funzione incognita, x=x(t)x' = x'(t) è la sua derivata prima e tt è la variable indipendente. Se siete studenti di fisica a Sapienza e state affrontando per la prima volta questo corso, avrete probabilmente già seguito (e magari passato con profitto) il corso di Meccanica. Durante il corso avrete senz’altro incontrato equazioni differenziali del secondo ordine del tipo

d2xdt2=a(x,v,t),\odd{\vec{x}}{t} = \vec{a}(\vec{x}, \vec{v}, t),

dove x(t)\vec{x}(t) è la posizione di un punto materiale, a(x,v,t)=F(x,v,t)/m\vec{a}(\vec{x}, \vec{v}, t) = \vec{F}(\vec{x}, \vec{v}, t) / m la sua accelerazione, F(t,x,v)\vec{F}(t, \vec{x}, \vec{v}) la forza a cui è sottoposto e mm la sua massa[2]. Ricordando il legame tra velocità e posizione, un’equazione del tipo (2) può essere trasformata in nel seguente sistema di equazioni del primo ordine:

{dxdt=v(t)dvdt=a(x,v,t),\begin{cases} \od{\vec{x}}{t} = \vec{v}(t)\\ \od{\vec{v}}{t} = \vec{a}(\vec{x}, \vec{v}, t), \end{cases}

cui di solito si affiancano le condizioni iniziali v(t0)=v0\vec{v}(t_0) = \vec{v}_0 e x(t0)=x0\vec{x}(t_0) = \vec{x}_0.

2.1Qualche esempio di sistemi non risolvibili analiticamente

Quando si inizia lo studio della fisica teorica, si ha spesso l’illusione che ogni sistema fisico descrivibile tramite equazioni di Newton o di Lagrange possa essere risolto “con carta e penna”, trovando una formula esatta per la traiettoria nel tempo. La realtà, purtroppo, è ben diversa: i sistemi integrabili analiticamente rappresentano una piccolissima eccezione in un oceano di problemi matematicamente intrattabili.

Per capire quanto sia facile imbattersi in equazioni prive di soluzioni analitiche, proviamo a scendere dal complesso al semplice, partendo da un sistema apparentemente elementare: il doppio pendolo.

2.1.1Il doppio pendolo: il regno del caos

Immaginiamo di appendere un pendolo rigido (la cui lunghezza, cioè, rimane costante) all’estremità di un altro. Questo sistema, composto da due aste rigide di lunghezza l1,l2l_1, l_2 e due masse m1,m2m_1, m_2 vincolate a muoversi su un piano verticale, ha solo due gradi di libertà, rappresentati dagli angoli θ1(t)\theta_1(t) e θ2(t)\theta_2(t) che le aste formano con la verticale.

Nonostante l’apparente semplicità costruttiva, la dinamica del sistema è determinata da un sistema di due equazioni differenziali del secondo ordine fortemente accoppiate e non lineari:

{d2θ1dt2=g(2m1+m2)sinθ1m2gsin(θ12θ2)2m2sin(Δθ)[(dθ2dt)2l2+(dθ1dt)2l1cos(Δθ)]l1[2m1+m2m2cos(2θ12θ2)]d2θ2dt2=2sin(Δθ)[(dθ1dt)2l1M+gMcosθ1+(dθ2dt)2l2m2cos(Δθ)]l2[2m1+m2m2cos(2θ12θ2)]\begin{cases} \odd{\theta_1}{t} = \frac{-g (2m_1 + m_2) \sin\theta_1 - m_2 g \sin(\theta_1 - 2\theta_2) - 2 m_2 \sin(\Delta \theta) \left[ \left(\od{\theta_2}{t}\right)^2 l_2 + \left(\od{\theta_1}{t}\right)^2 l_1 \cos(\Delta \theta) \right]}{l_1 \left[ 2m_1 + m_2 - m_2 \cos(2\theta_1 - 2\theta_2) \right]} \\ \odd{\theta_2}{t} = \frac{2 \sin(\Delta \theta) \left[ \left(\od{\theta_1}{t}\right)^2 l_1 M + g M \cos\theta_1 + \left(\od{\theta_2}{t}\right)^2 l_2 m_2 \cos(\Delta \theta) \right]}{l_2 \left[ 2m_1 + m_2 - m_2 \cos(2\theta_1 - 2\theta_2) \right]} \end{cases}

dove Δθ=θ1θ2\Delta \theta = \theta_1 - \theta_2 e M=m1+m2M = m_1 + m_2. Queste equazioni sono impossibili da risolvere in forma chiusa. Non solo: il doppio pendolo è uno dei più celebri esempi di sistema caotico, una proprietà che discuteremo meglio più avanti. Qui basti sapere che con “sistema caotico” si intende un sistema per cui una piccolissima variazione nelle condizioni iniziali θ1(0)\theta_1(0) o θ2(0)\theta_2(0) (anche solo dovuta alla precisione finita con cui un computer immagazzina i numeri decimali) produce traiettorie che divergono completamente[3]. Per studiarne la dinamica, l’integrazione numerica al computer non è un’opzione comoda, è l’unica via percorribile. Un esempio di simulazione è mostrato in Figura 1.

Figure 1:Simulazione di un pendolo doppio di parametri l1=l2=1l_1 = l_2 = 1 m, m1=0.2m_1 = 0.2 Kg e m2=0.1m_2 = 0.1 Kg e condizioni iniziali θ1,0=170\theta_{1,0} = 170^\circ, θ2,0=0\theta_{2,0} = 0^\circ, ω1,0=ω2,0=0\omega_{1,0} = \omega_{2,0} = 0.

2.1.2Il pendolo semplice

Si potrebbe pensare che la difficoltà del doppio pendolo derivi esclusivamente dall’accoppiamento tra i suoi due gradi di libertà. Eliminiamo allora il secondo pendolo, ponendo formalmente m2=0m_2=0. Rimane il pendolo semplice: una massa mm vincolata a muoversi lungo un arco di circonferenza di raggio LL. L’equazione del moto è

d2θdt2+gLsinθ=0.\odd{\theta}{t} + \frac{g}{L}\sin\theta=0.

Il sistema possiede un solo grado di libertà e non presenta alcun accoppiamento. Tuttavia, l’equazione è ancora non lineare a causa del termine sinθ\sin\theta. Questa non linearità non rende il problema caotico ma impedisce in generale di esprimere il moto mediante le sole funzioni elementari.

Per comprenderne l’origine, moltiplichiamo l’equazione per θ˙\dot{\theta}:

dθdtd2θdt2+gLsinθdθdt=0.\od{\theta}{t}\odd{\theta}{t} +\frac{g}{L}\sin\theta \od{\theta}{t} = 0.

Integrando rispetto al tempo si ottiene la conservazione dell’energia:

12(dθdt)2gLcosθ=C.\frac{1}{2}\left(\od{\theta}{t}\right)^2 - \frac{g}{L}\cos\theta=C.

Sia θ0\theta_0 l’ampiezza massima dell’oscillazione. Nel punto di inversione il pendolo è istantaneamente fermo, quindi

θ=θ0,θ˙=0,\theta=\theta_0, \qquad \dot{\theta}=0,

e pertanto

C=gLcosθ0.C=-\frac{g}{L}\cos\theta_0.

Segue che

(dθdt)2=2gL(cosθcosθ0).\left(\od{\theta}{t}\right)^2 = \frac{2g}{L} \left(\cos\theta-\cos\theta_0\right).

Questa relazione consente di determinare il tempo mediante un’integrazione. In particolare, il tempo necessario affinché il pendolo vada dall’ampiezza massima θ0\theta_0 alla posizione di equilibrio θ=0\theta=0 è pari a un quarto del periodo:

T4=L2g0θ0dθcosθcosθ0.\frac{T}{4} = \sqrt{\frac{L}{2g}} \int_0^{\theta_0} \frac{d\theta} {\sqrt{\cos\theta-\cos\theta_0}}.

L’integrale a destra è un integrale ellittico di prima specie. Non esiste alcuna manipolazione algebrica o sostituzione trigonometrica in grado di risolverlo usando le funzioni standard (come logaritmi, esponenziali, seni o coseni). Di fatto, le cosiddette funzioni ellittiche usate in matematica avanzata sono definite proprio a partire da questo tipo di integrali, il che equivale a dire che dobbiamo “inventarci” delle nuove funzioni per descrivere la soluzione.

2.1.3Le piccole oscillazioni

Com’è possibile, allora, che in tutti i corsi di fisica scolastici e universitari di base si impari a risolvere il pendolo con una semplice funzione trigonometrica?

Ciò è possibile solo introducendo un’approssimazione fisica cruciale: l’ipotesi di piccole oscillazioni. Se limitiamo lo studio a angoli molto piccoli (θ1\theta \ll 1 radiante, indicativamente sotto i 1010^\circ), possiamo sviluppare in serie di Taylor la funzione seno attorno a zero, arrestandoci al primo ordine:

sinθθ\sin\theta \approx \theta

Sotto questa assunzione, l’equazione del moto perde la sua natura non lineare e si trasforma in un’equazione differenziale lineare a coefficienti costanti:

d2θdt2+gLθ=0\odd{\theta}{t} + \frac{g}{L} \theta = 0

Questa equazione è finalmente risolvibile con carta e penna, e la sua soluzione generale è una semplice oscillazione armonica di frequenza ω0=g/L\omega_0 = \sqrt{g/L}:

θ(t)=θ0cos(ω0t+ϕ).\theta(t) = \theta_0 \cos(\omega_0 t + \phi).

In questa approssimazione, l’integrale ellittico nell’equazione (11) si riduce a una costante (π/2\approx \pi / \sqrt{2}), e il periodo diventa

T=2πLg.T = 2 \pi \sqrt{\frac{L}{g}}.

Questo modello lineare, noto come oscillatore armonico, è uno dei pilastri della fisica proprio perché rappresenta il “porto sicuro” in cui i fisici rifugiano ogni volta che un sistema non lineare diventa matematicamente inaffrontabile. Ed è proprio dall’oscillatore armonico che partiremo per testare e confrontare i nostri algoritmi di integrazione numerica.

2.2Il sistema modello per definizione: l’oscillatore armonico

Abbiamo appena visto come il pendolo in regime di piccole oscillazioni possa essere approssimato con un oscillatore armonico unidimensionale. Nel seguito, come esempio di sistema dinamico utilizzeremo proprio questo modello che, oltre a descrivere direttamente numerosi fenomeni fisici, presenta il vantaggio di possedere una soluzione analitica semplice, che potrà essere utilizzata per valutare l’accuratezza dei diversi algoritmi di integrazione numerica.

Consideriamo una particella di massa mm soggetta a una forza elastica proporzionale allo spostamento dalla posizione di equilibrio,

F=kx,F = -kx,

dove kk è la costante elastica della molla. Applicando la seconda legge di Newton, e usando la notazione xx' e xx'' per indicare le derivate prime e seconde, rispettivamente, otteniamo

mx=kx,m x'' = -kx,

ovvero

x=ω02x,x'' = -\omega_0^2 x,

dove abbiamo introdotto la pulsazione naturale del sistema

ω0km,\omega_0 \equiv \sqrt{\frac{k}{m}},

che implica come il periodo del moto sia

T=2πω0=2πmk.T = \frac{2\pi}{\omega_0} = 2\pi \sqrt{\frac{m}{k}}.

La soluzione generale di questa equazione è

x(t)=Acos(ω0t)+Bsin(ω0t)v(t)=Aω0sin(ω0t)+Bω0cos(ω0t),\begin{split} x(t) & = A \cos(\omega_0 t) + B \sin(\omega_0 t)\\ v(t) & = -A \omega_0 \sin(\omega_0 t) + B \omega_0 \cos(\omega_0 t), \end{split}

oppure, in forma equivalente,

x(t)=Ccos(ω0t+ϕ)v(t)=Cω0sin(ω0t+ϕ),\begin{split} x(t) & = C \cos(\omega_0 t + \phi)\\ v(t) & = -C \omega_0 \sin(\omega_0 t + \phi), \end{split}

dove le costanti AA, BB (oppure CC e ϕ\phi) sono determinate dalle condizioni iniziali, x(0)=x0x(0) = x_0 e v(0)=v0v(0) = v_0. Poiché si tratta di un sistema senza attrito sottoposto a una forza che non dipende esplicitamente dal tempo, l’energia totale, somma di energia potenziale U(t)U(t) ed energia cinetica K(t)K(t), si conserva:

E(t)=U(t)+K(t)=12kx2(t)+12mv2(t).E(t) = U(t) + K(t) = \frac{1}{2} kx^2(t) + \frac{1}{2} m v^2(t).

Come accennato precedentemente, per risolvere numericamente l’equazione differenziale del secondo ordine (23) conviene trasformarla nel seguente sistema di due equazioni del primo ordine:

{x=v,v=ω02x.\begin{cases} x' = v, \\ v' = -\omega_0^2 x. \end{cases}

Nel resto del capitolo considereremo anche una versione più generale del problema, che include sia l’attrito viscoso sia una forzante, ovvero una forza esterna dipendente dal tempo:

x=ω02xγx+F(t)m.x'' = -\omega_0^2 x - \gamma x' + \frac{F(t)}{m}.

A seconda della scelta dei parametri si ottengono diversi casi di interesse fisico:

Questo sistema costituirà il principale banco di prova per gli algoritmi di integrazione numerica discussi nelle sezioni successive.

2.3Dal continuo al discreto

I computer sono macchine discrete e, come tali, non possono rappresentare esattamente quantità continue, ma soltanto approssimarle mediante un numero finito di valori. Quando vogliamo studiare numericamente un sistema descritto da equazioni differenziali, dobbiamo quindi trovare un modo per tradurre un problema continuo in una forma compatibile con l’architettura discreta del calcolatore.

Esistono diverse strategie per affrontare questo problema; in queste note ci concentreremo sui cosiddetti metodi delle differenze finite (finite difference methods)[5]. Per semplicità cominciamo la trattazione considerando casi unidimensionali, in cui la funzione incognita è x(t)x(t). L’idea fondamentale consiste nel sostituire il dominio continuo della variabile indipendente (ad esempio il tempo tt) con una successione discreta di punti separati da un intervallo Δt\Delta t. In altre parole, invece di descrivere l’evoluzione del sistema in ogni istante, ne consideriamo soltanto una sequenza di “fotogrammi” successivi. Senza perdità di generalità, consideriamo un intervallo temporale [t0,tmax][t_0, t_{\rm max}] e suddividiamolo in NN intervalli uguali. Definiamo

Δttmaxt0N\Delta t \equiv \frac{t_{\rm max} - t_0}{N}

e i punti della griglia

tn=t0+nΔt,n=0,1,,N.t_n = t_0 + n \Delta t, \qquad n = 0, 1, \ldots, N.

Nel seguito per alleggerire la trattazione utilizzeremo spesso la notazione y(ti)=y(t0+iΔt)=yiy(t_i) = y(t_0 + i\Delta t) = y_i.

Una volta discretizzato il dominio, le derivate che compaiono nelle equazioni differenziali possono essere approssimate mediante opportune differenze tra i valori della funzione nei punti della griglia. Ad esempio, la derivata prima di x(t)x(t) nel punto tdt_d può essere approssimata come

x(td)=xd=dxdtt=tdxd+1xdΔt.x'(t_d) = x'_d = \left.\od{x}{t}\right|_{t=t_d} \approx \frac{x_{d+1}-x_d}{\Delta t}.

In questo modo un’equazione differenziale viene trasformata in una relazione algebrica tra valori della funzione calcolati in istanti successivi. Per comprendere l’idea alla base tutti i metodi che introdurremo nelle prossime sezioni, integriamo entrambi i membri dell’equazione (1) tra due istanti consecutivi della griglia temporale, tnt_n e tn+1t_{n+1}, ottenendo

xn+1xn=tntn+1f(x,t)dt,x_{n+1} - x_n = \int_{t_n}^{t_{n+1}} f(x,t) dt,

ovvero

xn+1=xn+tntn+1f(x,t)dt.x_{n+1} = x_n + \int_{t_n}^{t_{n+1}} f(x,t) dt.

Introducendo il passo temporale Δt=tn+1tn\Delta t = t_{n+1} - t_n, possiamo riscrivere questa espressione come

xn+1=xn+Δtfn,x_{n+1} = x_n + \Delta t \langle f \rangle_n,

dove

fn1Δttntn+1f(x,t)dt\langle f \rangle_n \equiv \frac{1}{\Delta t} \int_{t_n}^{t_{n+1}} f(x,t) dt

rappresenta il valore medio di f(x,t)f(x,t) nell’intervallo [tn,tn+1][t_n,t_{n+1}].

La relazione (36) è esatta e costituisce il punto di partenza di tutti i metodi di integrazione numerica che vedremo. La difficoltà risiede nel fatto che, in generale, il valore medio fn\langle f \rangle_n non è noto, poiché dipende dall’andamento della soluzione all’interno dell’intervallo stesso. I diversi algoritmi che presenteremo possono essere interpretati come diversi modi di approssimare questa quantità.

3Eulero e Eulero-Cromer

3.1Formulazione

Il metodo di Eulero è il più semplice algoritmo di integrazione numerica per equazioni differenziali ordinarie. L’idea consiste nell’approssimare il valore medio della funzione f(x,t)f(x,t) nell’intervallo [tn,tn+1][t_n,t_{n+1}] con il suo valore all’inizio dell’intervallo:

fnf(xn,tn).\langle f \rangle_n \approx f(x_n,t_n).

Sostituendo questa approssimazione nell’equazione (36) si ottiene

xn+1=xn+Δtf(xn,tn).x_{n+1} = x_n + \Delta t f(x_n,t_n).

L’algoritmo può quindi essere interpretato come un’estrapolazione lineare della soluzione a partire dalla sua derivata nel punto iniziale dell’intervallo.

Nel caso di ODE del secondo ordine, come l’oscillatore armonico, le equazioni di aggiornamento diventano

{xn+1=xn+vnΔtvn+1=vn+anΔt,\begin{cases} x_{n+1} = x_n + v_n \Delta t\\ v_{n+1} = v_n + a_n \Delta t, \end{cases}

dove ana(xn,vn,tn)a_n \equiv a(x_n,v_n,t_n). Abbiamo quindi approssimato sia l’accelerazione media sia la velocità media nell’intervallo [tn,tn+1][t_n,t_{n+1}] utilizzando i rispettivi valori all’inizio dell’intervallo, cioè

{ananvnvn.\begin{cases} \langle a \rangle_n \approx a_n\\ \langle v \rangle_n \approx v_n. \end{cases}

Il metodo di Eulero è semplice da implementare, ma la sua accuratezza è limitata e può produrre risultati qualitativamente scorretti quando viene applicato a sistemi oscillanti per tempi lunghi. Un miglioramente a volte sostanziale si può ottenere utilizzando il metodo di Eulero-Cromer, che è una semplice modifica del metodo di Eulero particolarmente adatta allo studio di sistemi meccanici.

Dal punto di vista concettuale, il metodo di Eulero-Cromer utilizza il valore iniziale dell’accelerazione per stimare l’accelerazione media (anan\langle a\rangle_n \approx a_n, come Eulero), ma il valore finale della velocità per stimare la velocità media (vnvn+1\langle v \rangle_n \approx v_{n+1}). L’algoritmo completo assume pertanto la forma

{vn+1=vn+anΔtxn+1=xn+vn+1Δt.\begin{cases} v_{n+1} = v_n + a_n\Delta t\\ x_{n+1} = x_n + v_{n+1}\Delta t. \end{cases}

Questa semplice modifica produce risultati significativamente migliori in molti problemi meccanici, in particolare nei sistemi oscillanti.

3.2C: Raggruppare i dati con struct e definire nuovi tipi con typedef

Per simulare l’oscillatore armonico dobbiamo conservare diverse quantità. Alcune descrivono lo stato del sistema in un particolare istante, come posizione e velocità; altre rimangono costanti durante la simulazione, come la massa, la costante elastica e il passo temporale.

Finora potremmo rappresentare queste quantità mediante variabili indipendenti:

double x;
double v;
double k;
double m;
double dt;

Questo approccio funziona, ma non rende esplicito il fatto che x e v descrivono insieme lo stato di un unico punto materiale, mentre k, m e dt caratterizzano il sistema che stiamo simulando.

Il C permette di raggruppare variabili logicamente collegate mediante una struttura, definita dalla parola chiave struct:

struct Punto {
    double x;
    double v;
};

Abbiamo così definito un nuovo tipo di struttura chiamato struct Punto. Possiamo usarlo per dichiarare una variabile che contiene contemporaneamente posizione e velocità:

struct Punto p;

p.x = 2.0;
p.v = 1.0;

Le variabili contenute in una struttura prendono il nome di membri o campi. Per accedere a un campo si usa l’operatore punto (.): p.x indica la posizione e p.v la velocità.

3.2.1Dare un nome a un tipo: typedef

In C, typedef permette di assegnare un nuovo nome a un tipo già esistente. Per evitare di dover scrivere ogni volta struct Punto, possiamo definire:

typedef struct Punto Punto;

Da questo momento Punto è un sinonimo di struct Punto, e possiamo scrivere più semplicemente:

Punto punto;

È molto comune unire la definizione della struttura e il typedef in un’unica dichiarazione:

typedef struct {
    double x;
    double v;
} Punto;

La parte racchiusa tra parentesi graffe descrive la struttura del dato, mentre il nome dopo la parentesi graffa, Punto, è il nuovo tipo che stiamo definendo.

Possiamo anche inizializzare tutti i campi al momento della dichiarazione:

Punto punto = {
    .x = 2.0,
    .v = 1.0
};

Le espressioni .x = 2.0 e .v = 1.0 sono dette inizializzatori designati. Rendono esplicito quale valore viene assegnato a ciascun campo.

3.2.2Separare lo stato dal sistema

Definiamo ora una seconda struttura che contenga i parametri necessari per descrivere l’oscillatore armonico e la sua integrazione numerica:

typedef struct {
   // a scelta, potremmo scrivere il problema in funzione della pulsazione
   // e tenere in memoria omega_0 invece di k
   double k;
   double m;
   double dt;
} Sistema;

Possiamo quindi inizializzare l’intera simulazione nel modo seguente:

Punto punto = {
    .x = 2.0,
    .v = 1.0
};

Sistema sistema = {
    .k = 1.0,
    .m = 1.0,
    .dt = 0.01
};

Le due strutture hanno ruoli diversi:

3.3Passare una struttura a una funzione

Le strutture possono essere passate alle funzioni come le variabili di tipo fondamentale. Per esempio, possiamo scrivere una funzione che calcoli l’accelerazione:

double accelerazione(Sistema sistema, Punto punto) {
    return -(sistema.k / sistema.m) * punto.x;
}

Questa funzione riceve però delle copie delle due strutture. Per evitare la copia possiamo passarne gli indirizzi:

double accelerazione(Sistema *sistema, Punto *punto) {
    return -(sistema->k / sistema->m) * punto->x;
}

Quando possediamo un puntatore a una struttura, accediamo ai suoi campi tramite l’operatore freccia (->). Per esempio,

punto->x

è una forma più leggibile ma del tutto equivalente a

(*punto).x

Possiamo ora riscrivere un passo del metodo di Eulero facendo sì che la funzione modifichi direttamente lo stato del punto:

void eulero(Sistema *sistema, Punto *punto) {
    double a = accelerazione(punto, sistema);

    punto->x += punto->v * sistema->dt;
    punto->v += a * sistema->dt;
}

Il metodo di Eulero-Cromer ha la stessa interfaccia e differisce soltanto nell’ordine degli aggiornamenti:

void eulero_cromer(Punto *punto, const Sistema *sistema) {
    double a = accelerazione(punto, sistema);

    punto->v += a * sistema->dt;
    punto->x += punto->v * sistema->dt;
}

Il ciclo temporale diventa così particolarmente compatto:

double t = 0.0;
Punto punto;
Sistema sistema;

/* inizializzazione */

while(t < t_max) {
    printf("%g %g %g\n", t, punto.x, punto.v);

    eulero_cromer(&sistema, &punto);
    t += sistema.dt;
}

Raggruppare i dati in strutture non cambia l’algoritmo, ma rende più chiaro quali quantità appartengano allo stato del sistema e quali ne definiscano le proprietà. Inoltre, per aggiungere nuove coordinate o nuovi parametri sarà sufficiente estendere la struttura corrispondente, senza modificare la firma di tutte le funzioni che la utilizzano.

3.4Esempio di integrazione numerica

<Figure size 700x600 with 3 Axes>

Figure 2:Il risultato dell’integrazione del sistema (29) con il metodo di Eulero. Dall’alto verso il basso, i tre pannelli mostrano la posizione x(t)x(t), la velocità y(t)y(t) e l’energia meccanica E(t)E(t) in funzione del tempo per tre diversi valori del passo temporale Δt\Delta t (10-1 in blu, 10-2 in arancione e 10-3 in verde), oltre al risultato esatto (riga tratteggiata). Considerando, per comodità, grandezze adimensionali, il sistema simulato ha k=m=1k = m = 1 (e quindi ω0=1\omega_0 = 1) e, come condizioni iniziali, x0=2x_0 = 2 e v0=1v_0 = 1.

Applichiamo i due metodi appena introdotti al sistema di equazioni differenziali (29), cercando di valutare la qualità della soluzione numerica discretizzata al variare della grandezza del passo temporale Δt\Delta t.

Cominciamo ad analizzare i risultati ottenuti con il metodo di Eulero, mostrati in Figura 2. Notiamo prima di tutto che solo le curva verdi (relative a Δt=103\Delta t = 10^{-3}) sembrano ricalcare fedelmente, almeno alla scala della figura, la soluzione teorica. Per valori maggiori di Δt\Delta t tutte le quantità mostrate si discostano anche sensibilmente dalla teoria. È preoccupante non tanto il fatto che ci sia una differenza tra i valori numerici e quelli teorici, quanto che questa differenza aumenti nel tempo. Infatti, una delle principali proprietà dell’oscillatore armonico è la sua periodicità: il moto si ripete esattamente ogni periodo T=2π/ω0T = 2 \pi / \omega_0. Come si può vedere dalla figura, questa proprietà non è affatto rispettata dalla soluzione ottenuta con il metodo di Eulero: le oscillazioni di posizione e velocità aumentano di ampiezza col tempo. Questo aumento si riflette nell’energia totale, che a sua volta aumenta monotonicamente: l’errore dovuto alla discretizzazione ha l’effetto netto di immettere energia nel sistema.

<Figure size 700x600 with 3 Axes>

Figure 3:Risultati analoghi a quelli di Figura 2, ottenuti però con il metodo di Eulero-Cromer. Notate l’intervallo dell’asse y del pannello di E(t)E(t), decisamente più ristretto rispetto a quello della Figura 2.

Passiamo ad analizzare i risultati ottenuti con Eulero-Cromer e mostrati in Figura 3. Nonostante l’apparente similitudine dei due metodi, il comportamento che si osserva è molto diverso. In questo caso posizione e velocità sembrano venir riprodotte quasi perfettamente per tutti i valori di Δt\Delta t, almeno alla scala della figura[6]. Per quanto riguarda l’energia, questa sembra comportarsi in una maniera più strana: in tutti i casi (anche se, per Δt=101\Delta t = 10^{-1}, non si vede bene) E(t)E(t) non è costante nel tempo ma oscilla con periodo uguale a quello di x(t)x(t) e v(t)v(t) e ampiezza che decresce al diminuire di Δt\Delta t. Quindi, se da un lato è vero che l’energia non si conserva, il suo valore medio rimane costante nel tempo: non c’è immissione o dissipazione netta di energia. Questa proprietà di “conservazione media” dell’energia è il massimo che possiamo chiedere a un algoritmo di integrazione numerico.

Figure 4:Simulazione di un oscillatore armonico integrato con Eulero (pallina rossa) ed Eulero-Cromer (pallina blu). I parametri della simulazione sono ω02=k/m=10\omega_0^2 = k / m = 10 s2^{-2}, x0=2x_0 = 2 m, v0=1v_0 = 1 m/s e Δt=0.01\Delta t = 0.01 s.

La Figura 4 contiene una simulazione interattiva che mostra come Eulero, a differenza di Eulero-Cromer, non riesca a riprodurre la periodicità dell’oscillatore armonico: si vede chiaramente come l’energia del sistema aumenti via via che il tempo passa, mostrando un comportamento evidentemente non fisico.

Il confronto fatto tra i risultati ottenuti con Eulero ed Eulero-Cromer ci permette di introdurre due proprietà fondamentali degli algoritmi per l’integrazione numerica: stabilità e accuratezza. Questi due concetti non sono necessariamente legati: un algoritmo può essere poco stabile ma molto accurato, un altro molto stabile ma poco accurato.

3.5Stabilità

Un algoritmo di integrazione numerica si dice stabile se piccoli errori introdotti durante l’evoluzione (dovuti, ad esempio, all’approssimazione del metodo o all’arrotondamento numerico, che sono fonti di errore sempre presenti su un calcolatore) non vengono amplificati in modo incontrollato al procedere dei passi temporali. Un metodo si dice incondizionatamente stabile se rimane stabile per qualunque valore del passo temporale Δt\Delta t. Si dice invece condizionatamente stabile se la stabilità è garantita solo quando Δt\Delta t soddisfa una certa condizione, ad esempio Δt<Δtmax\Delta t < \Delta t_{\rm max}. Sia la proprietà di essere condizionatamente/incodizionatamente stabile che l’eventuale valore di Δtmax\Delta t_{\rm max} dipendono non solo dall’algoritmo, ma anche dal problema che vogliamo risolvere. Vediamo come studiare la stabilità nel caso dell’oscillatore armonico, un sistema lineare che rende questo tipo di analisi più trasparente.

Le equazioni di aggiornamento del metodo di Eulero, Eq. (40), possono essere riscritte per l’oscillatore armonico come

{xn+1=xn+vnΔtvn+1=ω2xnΔt+vn.\begin{cases} x_{n+1} = x_n + v_n \Delta t\\ v_{n+1} = - \omega^2 x_n \Delta t + v_n. \end{cases}

Introduciamo ora il concetto di spazio delle fasi: questo è l’insieme di tutte le possibili configurazioni (o stati) del sistema. Nel caso dell’oscillatore armonico unidimensionale[7], per identificare una configurazione è sufficiente specificare posizione xx e velocità vv, e quindi lo spazio delle fasi comprende l’intero piano (x,v)(x, v). Un punto su questo piano, cioè una configurazione del sistema, si può identificare tramite un vettore y(xv)\mathbf{y} \equiv \begin{pmatrix} x \\ v\end{pmatrix}. Discretizzando la notazione, possiamo definire lo stato del sistema al generico tempo tkt_k, yk(xkvk)\mathbf{y}_k \equiv \begin{pmatrix} x_k \\ v_k\end{pmatrix}, così da poter riscrivere il passo di integrazione temporale (43) in forma compatta:

yn+1=M^yn,\mathbf{y}_{n+1} = \hat{M} \mathbf{y}_n,

dove

M^=(1Δtω02Δt1.)\hat{M} = \begin{pmatrix} 1 & \Delta t\\ -\omega_0^2 \Delta t & 1. \end{pmatrix}

Utilizzando questo formalismo possiamo scrivere direttamente l’evoluzione del sistema dalle condizioni iniziali y0=(x0v0)\mathbf{y}_0 = \begin{pmatrix} x_0 \\ v_0\end{pmatrix} ad un generico tempo tnt_n come

yn+1=M^ny0.\mathbf{y}_{n+1} = \hat{M}^n \mathbf{y}_0.

Invece di calcolare la potenza nn-esima di M^\hat{M} componente per componente, possiamo utilizzare la decomposizione spettrale in autovalori e autovettori per ottenere direttamente l’operatore che determina l’evoluzione del sistema al tempo voluto. Poiché M^\hat{M} è una matrice 2×22\times2, essa ammette due autovalori λ1\lambda_1 e λ2\lambda_2, ai quali corrispondono due autovettori linearmente indipendenti v1\mathbf{v}_1 e v2\mathbf{v}_2, tali per cui:

M^v1=λ1v1,M^v2=λ2v2.\hat{M}\mathbf{v}_1 = \lambda_1 \mathbf{v}_1, \quad \hat{M}\mathbf{v}_2 = \lambda_2 \mathbf{v}_2.

Poiché i due autovettori formano una base dello spazio delle fasi, possiamo esprimere qualsiasi condizione iniziale y0\mathbf{y}_0 come una loro combinazione lineare:

y0=c1v1+c2v2,\mathbf{y}_0 = c_1 \mathbf{v}_1 + c_2 \mathbf{v}_2,

dove c1c_1 e c2c_2 sono coefficienti (in generale complessi) che dipendono dallo stato iniziale scelto. Sfruttando la linearità della matrice M^\hat{M}, l’applicazione ripetuta dell’operatore di evoluzione per nn passi si riduce a

yn=M^ny0=M^n(c1v1+c2v2)=c1M^nv1+c2M^nv2.\mathbf{y}_n = \hat{M}^n \mathbf{y}_0 = \hat{M}^n (c_1 \mathbf{v}_1 + c_2 \mathbf{v}_2) = c_1 \hat{M}^n \mathbf{v}_1 + c_2 \hat{M}^n \mathbf{v}_2.

Poiché per definizione di autovettore si ha M^nv=λnv\hat{M}^n \mathbf{v} = \lambda^n \mathbf{v}, otteniamo l’espressione formale per lo stato del sistema al passo nn:

yn=c1λ1nv1+c2λ2nv2.\mathbf{y}_n = c_1 \lambda_1^n \mathbf{v}_1 + c_2 \lambda_2^n \mathbf{v}_2.

Per comprendere a fondo il comportamento di questa equazione senza dover calcolare immediatamente λ\lambda e v\mathbf{v}, analizziamo il sistema da una prospettiva geometrica e strutturale, partendo dal determinante della matrice di evoluzione, che ha un significato geometrico profondo: rappresenta il fattore di scala con cui vengono modificate le aree (o i volumi) nello spazio delle fasi.

Consideriamo prima di tutto l’effetto che l’evoluzione temporale discreta ha sulla propagazione degli errori. Immaginiamo che a un certo passo kk l’elaboratore introduca un piccolissimo errore di arrotondamento δk\boldsymbol{\delta}_k sullo stato del sistema (ad esempio, a causa della rappresentazione a precisione finita dei numeri in virgola mobile, che in doppia precisione hanno errori tipici dell’ordine di ϵ1016\epsilon \sim 10^{-16}).

Lo stato numerico reale al generico tempo tkt_k diventa yk+δk\mathbf{y}_k + \boldsymbol{\delta}_k. Se decomponiamo questa perturbazione microscopica nella base degli autovettori possiamo scrivere

δk=ϵ1v1+ϵ2v2.\boldsymbol{\delta}_k = \epsilon_1 \mathbf{v}_1 + \epsilon_2 \mathbf{v}_2.

Dopo mm passi di calcolo, l’errore iniziale si sarà evoluto in:

M^mδk=ϵ1λ1mv1+ϵ2λ2mv2.\hat{M}^m \boldsymbol{\delta}_k = \epsilon_1 \lambda_1^m \mathbf{v}_1 + \epsilon_2 \lambda_2^m \mathbf{v}_2.

Ipotizziamo che λ1λ2\lambda_1 \geq \lambda_2, e consideriamo il caso λ1>1|\lambda_1| > 1. In queste condizioni, anche se l’errore iniziale ϵ1\epsilon_1 è microscopicamente irrilevante (per esempio 1016\approx 10^{-16}), il fattore λ1m\lambda_1^m, che cresce esponenzialmente con mm, può portare il termine di errore ϵ1λ1m\epsilon_1 \lambda_1^m a diventare dello stesso ordine di grandezza del segnale fisico (si veda il box qui sotto per una dimostrazione rigorosa). In questo regime, detto di instabilità, i risultati dell’integrazione numerica sono del tutto privi di senso.

Passiamo ora ad analizzare come la trasformazione determinata da M^\hat{M} agisce nello spazio delle fasi. Se consideriamo una regione di condizioni iniziali che racchiude un’area A0A_0 (ad esempio, un quadratino di stati possibili), dopo un passo di integrazione questa regione si deformerà in un parallelogramma la cui area A1A_1 sarà pari a:

A1=det(M^)A0A_1 = |\det(\hat{M})| A_0

Nei sistemi fisici reali conservativi, l’evoluzione temporale non espande né contrae lo spazio delle fasi. Questa proprietà geometrica fondamentale è nota in meccanica classica come teorema di Liouville. Affinché un algoritmo numerico sia un buon modello della fisica reale, deve rispettare questa struttura.

Nel caso di uno spazio delle fasi bidimensionale (come nel nostro caso), la conservazione dell’area corrisponde a richiedere che[9]

det(M^)=1.\det(\hat{M}) = 1.

Vediamo ora come si collega la conservazione dell’area con il comportamento dei singoli stati descritto dall’equazione (55). Dall’algebra lineare sappiamo che il determinante di una matrice è pari al prodotto dei suoi autovalori:

det(M^)=λ1λ2.\det(\hat{M}) = \lambda_1 \lambda_2.

Discutiamo prima il caso in cui l’equazione (63) non è rispettata. Se det(M^)<1\det(\hat{M}) < 1, aree (o volumi) dello spazio delle fasi si contraggono man mano che si evolvono nel tempo. In questo caso l’equazione (68) implica che almeno uno degli autovalori è minore di uno. Se l’altro ha modulo maggiore di uno si ricade nell’amplificazione dell’errore discussa prima. Se invece entrambi gli autovalori hanno modulo minore di uno, i termini λn\lambda^n tenderanno a zero per nn \to \infty. L’evoluzione numerica smorzerà artificialmente le oscillazioni, comportandosi come se nel sistema fosse presente un attrito fittizio non fisico.

Di converso, se det(M^)>1\det(\hat{M}) > 1, almeno uno degli autovalori deve avere modulo maggiore di 1 per via dell’equazione (68), e darà quindi luogo ad un’espansione verso l’infinito di aree (o volumi) dello spazio delle fasi, oltre che ad un’amplificazione incontrollata degli errori. In questo caso, dell’energia viene immessa artificialmente nel sistema.

D’altro canto, se l’equazione (63) è rispettata, allora l’area occupata da un insieme di stati nello spazio delle fasi rimane rigorosamente costante nel tempo. La condizione det(M^)=1\det(\hat{M}) = 1 è quindi una condizione necessaria per garantire la stabilità a lungo termine e la quasi-conservazione[10] dell’energia numerica, la cui violazione porta ad un’alterazione artificiale della fisica del sistema ad ogni passo temporale, con conseguenze più o meno gravi a seconda del sistema studiato. Questa proprietà geometrica è nota come simpletticità (e l’algoritmo di integrazione che ne è provvisto si dice simplettico).

Per un integratore simplettico, gli autovalori sono rigidamente vincolati dalla relazione λ1λ2=1\lambda_1 \lambda_2 = 1. Questo vincolo fa sì che esistano diversi scenari da analizzare.

Consideriamo il caso di due autovalori reali e diversi da 1. A causa del vincolo λ1λ2=1\lambda_1 \lambda_2 = 1, è impossibile che entrambi abbiano modulo unitario. Uno dei due autovalori (supponiamo λ1\lambda_1) dovrà essere maggiore di 1 in modulo, mentre l’altro (λ2\lambda_2) dovrà essere minore di 1. L’effetto geometrico combinato sulla dinamica del sistema prende il nome di strain (o deformazione a forbice):

L’area totale del parallelogramma nello spazio delle fasi si conserva (poiché la compressione bilancia esattamente l’allungamento), ma la forma si allunga indefinitamente come una striscia infinitamente sottile e lunga. Fisicamente, il sistema diverge ed “esplode”. In questo regime, l’algoritmo è numericamente instabile, in maniera del tutto simile al caso det(M^)>1\det(\hat{M}) > 1.

Questa divergenza catastrofica viene evitata quando gli autovalori non sono reali ma complessi e coniugati: λ1,2=λ,λˉ\lambda_{1,2} = \lambda, \bar{\lambda}. In questo caso, il vincolo del determinante si può scrivere come

λ1λ2=λλˉ=λ2=1    λ=1,\lambda_1 \lambda_2 = \lambda \bar{\lambda} = |\lambda|^2 = 1 \implies |\lambda| = 1,

cioè il modulo di entrambi gli autovalori deve essere esattamente pari a 1. Possiamo quindi scrivere gli autovalori in forma polare come λ1,2=e±iθ\lambda_{1,2} = e^{\pm i \theta}, che mostra esplicitamente come l’evoluzione temporale rappresenti una pura rotazione periodica nel piano complesso. Le traiettorie rimangono limitate e la simulazione è numericamente stabile: gli errori di arrotondamento non vengono amplificati, ma si limitano a oscillare insieme al sistema.

Per visualizzare concretamente il legame profondo tra la conservazione dell’area e la stabilità numerica, analizziamo ora l’animazione mostrata in Figura Figura 5, che confronta l’evoluzione di una regione dello spazio delle fasi secondo i metodi di Eulero ed Eulero-Cromer.

Loading...

Figure 5:L’evoluzione di un volume di spazio delle fasi (che per l’oscillatore armonico è un piano) delimitato da un rettangolo ottenuto con i metodi di Eulero (in rosso) ed Eulero-Cromer (in blu). I parametri della simulazione sono k=1k = 1, m=1m = 1 (quindi ω=1\omega = 1) e Δt=0.1\Delta t = 0.1.

L’animazione mostra come nelle condizioni di simulazione (cioè per i valori di ω\omega e Δt\Delta t utilizzati), l’algoritmo di Eulero mostra un’espansione dell’area dello spazio delle fasi, che invece non si verifica con Eulero-Cromer. Verifichiamo questi comportamenti calcolando esplicitamente determinanti ed autovalori associati all’oscillatore armonico integrato con i due metodi.

3.5.1Eulero

La matrice di propagazione per il metodo di Eulero è

M^E=(1Δtω02Δt1,)\hat{M}_E = \begin{pmatrix} 1 & \Delta t \\ -\omega_0^2 \Delta t & 1, \end{pmatrix}

da cui possiamo immediatamente ottenere il determinante:

det(M^E)=11(Δt)(ω02Δt)=1+ω02Δt2.\det(\hat{M}_E) = 1 \cdot 1 - (\Delta t)(-\omega_0^2 \Delta t) = 1 + \omega_0^2 \Delta t^2.

Poiché Δt>0\Delta t > 0 e ω0>0\omega_0 > 0, si ha che det(M^E)>1\det(\hat{M}_E) > 1 per qualunque valore di Δt\Delta t. Essendo il modulo strettamente maggiore di 1, l’errore globale cresce esponenzialmente a ogni passo temporale. Il metodo è quindi incondizionatamente instabile per l’oscillatore armonico; nello spazio delle fasi, la soluzione numerica descrive una spirale che diverge verso l’infinito, accumulando energia artificiale.

Poiché ω02Δt2\omega_0^2 \Delta t^2 è un numero strettamente positivo, il determinante della matrice è sempre maggiore di 1. Di conseguenza, il metodo di Eulero è incondizionatamente instabile per l’oscillatore armonico: l’ampiezza delle oscillazioni numeriche crescerà artificialmente all’infinito per qualunque scelta di Δt\Delta t. Nello spazio delle fasi, questo comportamento si manifesta come mostrato in figura Figura 5: la soluzione numerica descrive una spirale che diverge verso l’infinito, accumulando energia artificiale.

Calcoliamo ora gli autovalori di M^E\hat{M}_E. Risolvendo il polinomio caratteristico det(M^EλI^)=(1λ)2+ω02Δt2=0\det(\hat{M}_E - \lambda \hat{I}) = (1-\lambda)^2 + \omega_0^2 \Delta t^2 = 0 si trova (1λ)2=ω02Δt2(1-\lambda)^2 = -\omega_0^2 \Delta t^2, da cui si ottengono i due autovalori complessi coniugati

λ1,2=1±iω0Δt.\lambda_{1,2} = 1 \pm i \omega_0 \Delta t.

Poiché sono complessi coniugati, i due autovalori hanno lo stesso modulo, che vale[11]

λ1=λ2=1+ω02Δt2,|\lambda_1| = |\lambda_2| = \sqrt{1 + \omega_0^2 \Delta t^2},

cioè un numero maggiore di 1, indipendentemente dal passo di integrazione. Come abbiamo dimostrato precedentemente, se il modulo degli autovalori è strettamente maggiore di 1, l’errore cresce esponenzialmente, dimostrando ancora una volta l’instabilità del metodo di Eulero.

3.5.2Eulero-Cromer

Nel caso di Eulero-Cromer, la matrice di propagazione del metodo nello spazio delle fasi è

MEC=(1ω02Δt2Δtω02Δt1,)M_{EC} = \begin{pmatrix} 1 - \omega_0^2 \Delta t^2 & \Delta t \\ -\omega_0^2 \Delta t & 1, \end{pmatrix}

che ha determinante

det(MEC)=(1ω02Δt2)(1)(Δt)(ω02Δt)=1ω02Δt2+ω02Δt2=1\det(M_{EC}) = (1 - \omega_0^2 \Delta t^2)(1) - (\Delta t)(-\omega_0^2 \Delta t) = 1 - \omega_0^2 \Delta t^2 + \omega_0^2 \Delta t^2 = 1

Poiché det(MEC)=1\det(M_{EC}) = 1, il metodo conserva l’area nello spazio delle fasi, che per sistemi unidimensionali come l’oscillatore armonico implica simpletticità. Questo garantisce l’assenza di derive energetiche artificiali a lungo termine. In questo caso, il polinomio caratteristico è

λ2(2ω02Δt2)λ+1=0,\lambda^2 - (2 - \omega_0^2 \Delta t^2)\lambda + 1 = 0,

da cui si ottengono gli autovalori

λ1,2=(2ω02Δt2)±(2ω02Δt2)242=(2ω02Δt2)±ω0Δtω02Δt242.\lambda_{1,2} = \frac{(2-\omega_0^2 \Delta t^2) \pm \sqrt{(2-\omega_0^2 \Delta t^2)^2 - 4}}{2} = \frac{(2-\omega_0^2 \Delta t^2) \pm \omega_0 \Delta t \sqrt{\omega_0^2 \Delta t^2 - 4}}{2}.

Il comportamento del sistema dipende dal segno del radicando (ω02Δt24\omega_0^2 \Delta t^2 - 4):

  1. ω0Δt<2\omega_0 \Delta t < 2. Il radicando è negativo, producendo autovalori complessi coniugati. Poiché il determinante è unitario, e in forza all’equazione (68), i due autovalori devono avere anche modulo 1, e quindi trovarsi sulla circonferenza unitaria. In questo regime il metodo è stabile e genera orbite ellittiche chiuse nello spazio delle fasi.

  2. ω0Δt>2\omega_0 \Delta t > 2. Il radicando è positivo, quindi i due autovalori sono reali e distinti. Poiché il loro prodotto deve rimanere pari a 1, uno dei due autovalori sarà necessariamente maggiore di 1 in modulo: il sistema diventa instabile e l’errore diverge esponenzialmente. Questa dipendenza della stabilità dai parametri del sistema (e dell’integrazione numerica) fa sì che il metodo di Eulero-Cromer sia condizionatamente stabile. Nel caso in esame, la condizione di stabilità matematica, che richiede che gli autovalori abbiano modulo 1, impone infatti un limite superiore rigoroso al passo temporale:

    Δt<2ω0.\Delta t < \frac{2}{\omega_0}.

3.6Accuratezza

Per valutare la bontà (e quindi l’accuratezza) di un metodo di integrazione numerica è fondamentale distinguere tra due definizioni di errore:

3.6.1Eulero

La derivazione dell’accuratezza per il metodo di Eulero discende direttamente dallo sviluppo in serie di Taylor di posizione x(t)x(t) e velocità v(t)v(t) attorno all’istante tnt_n:

x(tn+1)=x(tn)+v(tn)Δt+12a(tn)Δt2+O(Δt3)v(tn+1)=v(tn)+a(tn)Δt+12da(tn)dtΔt2+O(Δt3).\begin{align} x(t_{n+1}) & = x(t_n) + v(t_n) \Delta t + \frac{1}{2} a(t_n) \Delta t^2+ O(\Delta t^3)\\ v(t_{n+1}) & = v(t_n) + a(t_n) \Delta t+ \frac{1}{2} \od{a(t_n)}{t} \Delta t^2 + O(\Delta t^3). \end{align}

Confrontando queste espressioni con le equazioni di aggiornamento dello schema di Eulero, eq. (40), si nota immediatamente che lo schema numerico recide i termini di Taylor a partire dal secondo ordine. Definendo l’errore come la differenza tra il dato locale esatto e quello ottenuto numericamente, l’errore locale di troncamento risulta:

x(tn+1)xn+1=12a(tn)+O(Δt3)Δt2=O(Δt2)v(tn+1)vn+1=12dadt(tn)Δt2+O(Δt3)=O(Δt2).\begin{align} x(t_{n+1}) - x_{n+1} &= \frac{1}{2} a(t_n) + O(\Delta t^3) \Delta t^2 = O(\Delta t^2)\\ v(t_{n+1}) - v_{n+1} &= \frac{1}{2} \od{a}{t}(t_n) \Delta t^2 + O(\Delta t^3) = O(\Delta t^2). \end{align}

Poiché l’errore locale è O(Δt2)O(\Delta t^2), l’accumulo globale su N1/ΔtN \propto 1/\Delta t passi produce un errore complessivo di ordine O(Δt)O(\Delta t). Eulero Esplicito è pertanto un metodo del primo ordine.

3.6.2Eulero-Cromer

Nel caso di Eulero-Cromer, lo schema definito in eq. (42) fa uso della velocità aggiornata al tempo successivo per calcolare la nuova posizione. Mentre la relazione per l’aggiornamento di vn+1v_{n+1} è identica a quella di Eulero Esplicito, e di conseguenza preserva un errore locale pari a O(Δt2)O(\Delta t^2), l’analisi della posizione richiede cautela. Sostituendo vn+1v_{n+1} nella definizione di xn+1x_{n+1}, possiamo scrivere l’espressione per la variabile xn+1x_{n+1} in funzione delle sole quantità al tempo tnt_n:

xn+1=xn+Δt(vn+Δtan)=xn+Δtvn+Δt2an.x_{n+1} = x_n + \Delta t (v_n + \Delta t a_n) = x_n + \Delta t v_n + \Delta t^2 a_n.

Confrontiamo ora questa equazione dello schema con lo sviluppo esatto di Taylor di x(tn+1)x(t_{n+1}) ricavato in precedenza. Calcolando la differenza, si ottiene l’errore di troncamento locale sulla posizione:

x(tn+1)xn+1=x(tn)+Δtv(tn)+Δt22a(tn)+O(Δt3)xn+Δtvn+Δt2an.x(t_{n+1}) - x_{n+1} = x(t_n) + \Delta t v(t_n) + \frac{\Delta t^2}{2} a(t_n) + O(\Delta t^3) - x_n + \Delta t v_n + \Delta t^2 a_n.

Imponendo l’esattezza dei dati al passo nn, i termini di ordine zero e primo si cancellano, lasciando la discrepanza unicamente sul coefficiente del secondo ordine:

x(tn+1)xn+1=12Δt2a(tn)+O(Δt3)=Δt22a(tn)+O(Δt3)=O(Δt2).x(t_{n+1}) - x_{n+1} = -\frac{1}{2} \Delta t^2 a(t_n) + O(\Delta t^3) = -\frac{\Delta t^2}{2} a(t_n) + O(\Delta t^3) = O(\Delta t^2).

Poiché l’errore locale di troncamento è pari a O(Δt2)O(\Delta t^2) sia per la velocità che per la posizione, l’integrazione accumula un errore globale proporzionale a O(Δt)O(\Delta t), esattamente come per il metodo di Eulero. Quindi, nonostante l’utilizzo di informazioni temporalmente più avanzate per la coordinata spaziale, il metodo di Eulero-Cromer rimane un metodo del primo ordine. L’errore locale sulla posizione ha lo stesso modulo di quello di Eulero Esplicito, ma segno opposto.

<Figure size 640x480 with 1 Axes>

Figure 6:La differenza tra la posizione finale teorica e quella ottenuta tramite i due algoritmi di Eulero ed Eulero-Cromer. Il tempo totale di simulazione è tf=20t_f = 20, mentre i parametri utilizzati sono k=1k = 1, m=1m = 1 (e quindi ω0=1\omega_0 = 1), x0=2x_0 = 2, v0=1v_0 = 1.

Se la soluzione teorica è nota (come in questo caso), l’errore si può anche calcolare direttamente dai risultati numerici. Possiamo infatti definire l’errore globale come

ϵG=xfx(tf),\epsilon_G = |x_f - x(t_f)|,

dove tft_f è il tempo finale, mentre xfx_f e x(tf)x(t_f) sono le posizioni ottenute numericamente e teoricamente. La Figura 6 mostra ϵG\epsilon_G per sistemi con ω0=1\omega_0 = 1, x0=2x_0 = 2 e v0=1v_0 = 1, simulati per tf=20t_f = 20 (in unità adimensionali) con Eulero ed Eulero-Cromer. La figura mostra come l’errore che commette Eulero-Cromer sia, in questo caso, di più di un ordine di grandezza minore rispetto a quello che si ottiene con Eulero, ma la dipendenza da Δt\Delta t è la stessa. Per grandi valori di Δt\Delta t l’errore di Eulero sembra crescere più che linearmente, per via dell’instabilità del metodo. Questo comportamento non si verifica con Eulero-Cromer dato che tutti i valori di Δt\Delta t considerati rispecchiano la relazione di stabilità condizionata, eq. (78).

4Velocity Verlet

Introduciamo ora uno dei metodi simplettici, cioè che conserva l’energia e il volume nello spazio delle fasi, migliori e più utilizzati. Sviluppiamo la posizione x(t)x(t) in serie di Taylor attorno a tt:

x(t+Δt)=x(t)+v(t)Δt+12a(t)Δt2+16dadtΔt3+O(Δt4)x(tΔt)=x(t)v(t)Δt+12a(t)Δt216dadtΔt3+O(Δt4)\begin{aligned} x(t + \Delta t) = x(t) + v(t) \Delta t + \frac{1}{2} a(t) \Delta t^2 + \frac{1}{6} \od{a}{t} \Delta t^3 + \mathcal{O}(\Delta t^4)\\ x(t - \Delta t) = x(t) - v(t) \Delta t + \frac{1}{2} a(t) \Delta t^2 - \frac{1}{6} \od{a}{t} \Delta t^3 + \mathcal{O}(\Delta t^4) \end{aligned}

Sommando i due sviluppi notiamo che, per simmetria, i termini con potenze dispari di Δt\Delta t si elidono e si ottiene

x(t+Δt)+x(tΔt)=2x(t)+a(t)Δt2+O(Δt4).x(t + \Delta t) + x(t - \Delta t) = 2x(t) + a(t) \Delta t^2 + \mathcal{O}(\Delta t^4).

Se trascuriamo i termini di ordine superiore O(Δt4)\mathcal{O}(\Delta t^4) e discretizziamo il tempo, ttnt \to t_n, l’aggiornamento della posizione diventa

xn+1=2xnxn1+anΔt2.x_{n+1} = 2x_n - x_{n-1} + a_n \Delta t^2.

Questo è l’algoritmo di Verlet, che permette di calcolare la posizione xn+1x_{n+1} al passo temporale successivo utilizzando la posizione corrente xnx_n, la posizione precedente xn1 x_{n-1} e l’accelerazione corrente ana_n. Sebbene scritto in questo modo il metodo di Verlet non coinvolge esplicitamente la velocità, se consideriamo lo sviluppo fino al secondo ordine possiamo scrivere esplicitamente

vn=xn+1xn12Δt+O(Δt2),v_n = \frac{x_{n+1} - x_{n-1}}{2\Delta t} + \mathcal{O}(\Delta t^2),

dove è importante la differenza di accuratezza (O(Δt2)\mathcal{O}(\Delta t^2) vs O(Δt4)\mathcal{O}(\Delta t^4)) rispetto a xx. Inoltre, per Δt\Delta t sufficientemente piccolo, xn+1x_{n+1} e xn1x_{n-1} saranno molto simili, quindi la loro differenza può dare problemi quando i numeri vengono rappresentanti sul calcolatore. Per questo motivo il metodo è raramente usato in questa forma, ma si modifica per includere un aggiornamento esplicito per la velocità, ottenendo il ben più comune metodo “Velocity Verlet”. Invece di basarsi sulle posizioni dei passi temporali precedente e corrente, l’algoritmo Velocity Verlet aggiorna la posizione e la velocità in un processo a due fasi.

In primo luogo, utilizziamo la velocità e l’accelerazione correnti per aggiornare la posizione al tempo t+Δtt + \Delta t. Questo viene fatto in modo simile al metodo Verlet di base, ma con il termine della velocità esplicitamente incluso:

xn+1=xn+vnΔt+12anΔt2x_{n+1} = x_n + v_n \Delta t + \frac{1}{2} a_n \Delta t^2

Questa equazione utilizza la posizione corrente xnx_n, la velocità corrente vnv_n e l’accelerazione corrente ana_n per calcolare la nuova posizione xn+1x_{n+1}. Successivamente, dopo aver aggiornato la posizione, dobbiamo calcolare la nuova accelerazione al tempo tn+1t_{n+1} perché la forza (e quindi l’accelerazione) è cambiata a causa della posizione aggiornata. La nuova accelerazione è data da:

an+1=Fn+1ma_{n+1} = \frac{F_{n+1}}{m}

Avendo a disposizione questa nuova accelerazione, possiamo aggiornare la velocità. Invece di usare solo l’accelerazione corrente, il metodo Velocity Verlet usa la media delle accelerazioni corrente e nuova per aggiornare la velocità:

vn+1=vn+12(an+an+1)Δtv_{n+1} = v_n + \frac{1}{2} (a_n + a_{n+1}) \Delta t

Questa equazione di aggiornamento della velocità tiene conto della variazione dell’accelerazione nell’intervallo di tempo, fornendo un aggiornamento della velocità più accurato rispetto al semplice utilizzo dell’accelerazione corrente. Il metodo Velocity Verlet è lo standard de facto per i codici di Dinamica Molecolare (MD), una tecnica utilizzata per studiare la dinamica e la termodinamica di atomi, molecole, colloidi, ecc. Il modo comune per implementarlo consiste nel suddividere la fase di integrazione della velocità in due, in modo che un passo di integrazione completo diventi:

  1. Aggiornamento della velocità, prima fase: vn+1/2=vn+12anΔtv_{n+1/2} = v_n + \frac{1}{2} a_n \Delta t.

  2. Aggiornamento della posizione: xn+1=xn+vn+1/2Δt=x(t)+vnΔt+12anΔt2x_{n+1} = x_n + v_{n+1/2}\Delta t = x(t) + v_n \Delta t + \frac{1}{2} a_n \Delta t^2 (cioè l’eq. (89)).

  3. Calcolo della forza (e quindi dell’accelerazione) utilizzando la nuova posizione: xn+1an+1=Fn+1/mx_{n+1} \to a_{n+1} = F_{n+1} / m.

  4. Aggiornamento della velocità, seconda fase: vn+1=vn+1/2+12an+1Δt=vn+12(an+an+1)Δtv_{n+1} = v_{n+1/2} + \frac{1}{2} a_{n+1}\Delta t = v_n + \frac{1}{2} (a_n + a_{n+1}) \Delta t (cioè l’eq. (91)).

4.1Stabilità ed accuratezza

Consideriamo le equazioni di aggiornamento del metodo Velocity Verlet:

{xn+1=xn+vnΔt+12anΔt2vn+1=vn+12(an+an+1)Δt.\begin{cases} x_{n+1} &= x_n + v_n \Delta t + \frac{1}{2} a_n \Delta t^2\\ v_{n+1} &= v_n + \frac{1}{2} (a_n + a_{n+1}) \Delta t. \end{cases}

Nel caso dell’oscillatore armonico, an=ω0xna_n = -\omega_0 x_n e an+1=ω0xn+1a_{n+1} = -\omega_0 x_{n+1}, quindi

{xn+1=xn+vnΔt12ω0xnΔt2vn+1=vn12ω0(xn+xn+1)Δt.\begin{cases} x_{n+1} &= x_n + v_n \Delta t - \frac{1}{2} \omega_0 x_n \Delta t^2\\ v_{n+1} &= v_n - \frac{1}{2} \omega_0(x_n + x_{n+1}) \Delta t. \end{cases}

Sostituendo la prima equazione nella seconda otteniamo

vn+1=vn12ω02Δtxn12ω02Δt[(112ω02Δt2)xn+Δtvn]v_{n+1} = v_n - \frac{1}{2} \omega_0^2 \Delta t x_n - \frac{1}{2} \omega_0^2 \Delta t \left[ \left(1 - \frac{1}{2} \omega_0^2 \Delta t^2\right) x_n + \Delta t v_n \right]

che, raccogliendo i termini associati a xnx_n e vnv_n, diventa:

vn+1=ω02Δt(114ω02Δt2)xn+(112ω02Δt2)vn.v_{n+1} = -\omega_0^2 \Delta t \left(1 - \frac{1}{4} \omega_0^2 \Delta t^2\right) x_n + \left(1 - \frac{1}{2} \omega_0^2 \Delta t^2\right) v_n.

La matrice di propagazione è quindi

M^VV=(112ω02Δt2Δtω02Δt(114ω02Δt2)112ω02Δt2.)\hat{M}_{VV} = \begin{pmatrix} 1 - \frac{1}{2} \omega_0^2 \Delta t^2 & \Delta t \\ -\omega_0^2 \Delta t \left(1 - \frac{1}{4} \omega_0^2 \Delta t^2\right) & 1 - \frac{1}{2} \omega_0^2 \Delta t^2. \end{pmatrix}

Calcoliamo il determinante della matrice:

det(M^VV)=(112ω02Δt2)2[ω02Δt2(114ω02Δt2)]==(1ω02Δt2+14ω04Δt4)+ω02Δt214ω04Δt4=1\begin{align} \det(\hat{M}_{\text{VV}}) &= \left(1 - \frac{1}{2} \omega_0^2 \Delta t^2\right)^2 - \left[ -\omega_0^2 \Delta t^2 \left(1 - \frac{1}{4} \omega_0^2 \Delta t^2\right) \right] = \\ &= \left( 1 - \omega_0^2 \Delta t^2 + \frac{1}{4} \omega_0^4 \Delta t^4 \right) + \omega_0^2 \Delta t^2 - \frac{1}{4} \omega_0^4 \Delta t^4 \\ &= 1 \end{align}

Quindi, il determinante è esattamente pari a 1, indipendentemente dal valore del passo temporale Δt\Delta t: Velocity Verlet è un algoritmo simplettico (conserva l’area nello spazio delle fasi) per qualsiasi parametro di discretizzazione scelto. Non introduce alcuna dissipazione o amplificazione artificiale dell’area di stati iniziali.

Studiato ora gli autovalori. Il polinomio caratteristico di Velocity Verlet è:

λ2(2ω02Δt2)λ+1=0.\lambda^2 - \left(2 - \omega_0^2 \Delta t^2\right)\lambda + 1 = 0.

Questa equazione vi ricorda qualcosa? Questa equazione è assolutamente identica a quella ottenuta con il metodo di Eulero-Cromer, eq. (76)!

Sebbene le due matrici di evoluzione siano diverse (Velocity Verlet ha coefficienti simmetrici sulla diagonale ed è un metodo del secondo ordine, mentre Eulero-Cromer è asimmetrico ed è del primo ordine), esse condividono lo stesso polinomio caratteristico e di conseguenza hanno gli stessi identici autovalori e quindi la stessa condizione di stabilità, eq. (78). La differenza maggiore tra i due metodi è nella loro accuratezza. Utilizzando la stessa logica applicata a Eulero ed Eulero-Cromer, definiamo l’errore di troncamento locale come la differenza tra la soluzione numerica e quella teorica dopo un passo di integrazione. Sviluppando posizione e velocità fino al quarto ordine, scriviamo le soluzioni esatte come

{x(tn+1)=x(tn)+v(tn)Δt+12a(tn)Δt2+6da(tn)dtΔt3+O(Δt4)v(tn+1)=v(tn)+a(tn)Δt+12da(tn)dtΔt2+16d2a(tn)dt2Δt3+O(Δt4).\begin{cases} x(t_{n+1}) & = x(t_n) + v(t_n) \Delta t + \frac{1}{2} a(t_n) \Delta t^2 + \frac{}{6} \od{a(t_n)}{t} \Delta t^3 + O(\Delta t^4)\\ v(t_{n+1}) & = v(t_n) + a(t_n) \Delta t + \frac{1}{2} \od{a(t_n)}{t} \Delta t^2 + \frac{1}{6} \odd{a(t_n)}{t} \Delta t^3 + O(\Delta t^4). \end{cases}

Se ora sottriamo queste quantità da quelle numeriche, eq. (92), e sostituiamo a an+1a_{n+1} il suo sviluppo di Taylor, an+1=a(tn)+a˙(tn)Δt+12a¨(tn)Δt2+O(Δt3)a_{n+1} = a(t_n) + \dot{a}(t_n) \Delta t + \frac{1}{2} \ddot{a}(t_n) \Delta t^2 + \mathcal{O}(\Delta t^3)[12], otteniamo

{xn+1x(tn+Δt)=16d3xdt3(tn)Δt3+O(Δt4)vn+1v(tn+Δt)=112d2a(tn)dt2Δt3+O(Δt4),\begin{cases} x_{n+1} - x(t_n + \Delta t) &= -\frac{1}{6} \frac{d^3 x}{dt^3}(t_n) \Delta t^3 + \mathcal{O}(\Delta t^4)\\ v_{n+1} - v(t_n + \Delta t) &= \frac{1}{12} \odd{a(t_n)}{t} \Delta t^3 + \mathcal{O}(\Delta t^4), \end{cases}

e quindi sia per la posizione che per la velocità, l’errore locale dell’algoritmo di Verlet (e quindi, equivalentemente, quello di Velocity Verlet) è di ordine O(Δt3)\mathcal{O}(\Delta t^3), che implica come l’errore globale scali come O(Δt2)\mathcal{O}(\Delta t^2).

<Figure size 640x480 with 1 Axes>

Figure 7:Come in Figura 6, con, in aggiunta, l’errore ottenuto applicando l’algoritmo di Velocity Verlet.

La Figura 7 illustra vividamente l’enorme impatto del passaggio da un errore globale di ordine O(Δt)\mathcal{O}(\Delta t) a uno di ordine O(Δt2)\mathcal{O}(\Delta t^2). Per apprezzare concretamente questa differenza, si consideri un passo temporale tipico delle simulazioni reali, ad esempio Δt=103\Delta t = 10^{-3}: in questo scenario, l’accuratezza di Velocity Verlet supera quella di Eulero-Cromer di ben tre ordini di grandezza, riducendo drasticamente l’errore sistematico accumulato sulla traiettoria.

4.2C: Puntatori a funzione, ovvero come scegliere l’algoritmo durante l’esecuzione

Finora abbiamo confrontato diversi algoritmi di integrazione, come Eulero, Eulero-Cromer e Velocity Verlet. Un modo poco pratico di farlo consiste nello scrivere un programma diverso per ogni algoritmo, oppure nel disseminare il ciclo di simulazione di istruzioni if. Questo duplica il codice che inizializza il sistema, esegue l’evoluzione e salva i risultati.

Sarebbe più comodo scrivere un solo ciclo di simulazione e scegliere l’algoritmo da usare all’inizio dell’esecuzione. In C possiamo farlo mediante un puntatore a funzione.

Consideriamo tre funzioni che eseguono un singolo passo temporale:

void eulero(Sistema *sist, Punto *p);
void eulero_cromer(Sistema *sist, Punto *p);
void velocity_verlet(Sistema *sist, Punto *p);

Le tre funzioni hanno la stessa firma: ricevono argomenti dello stesso tipo e restituiscono tutte void. Possiamo quindi definire un tipo che rappresenta un puntatore a una qualunque funzione con questa firma:

typedef void (*integratore)(Sistema *sist, Punto *p);

La dichiarazione può essere letta dall’interno verso l’esterno: integratore è un puntatore (*) a una funzione che riceve un puntatore a un Sistema e uno a un Punto e non restituisce alcun valore.

Possiamo ora dichiarare una variabile di questo tipo e farla puntare all’algoritmo scelto, per esempio utilizzando un argomento da riga di comando

integratore passo;

// vogliamo che il primo argomento determini l'algoritmo di integrazione
int scelta = atoi(argv[1]);

if(scelta == 1) {
    passo = eulero;
}
else if(scelta == 2) {
    passo = eulero_cromer;
}
else if(scelta == 3) {
    passo = velocity_verlet;
}
else {
    fprintf(stderr, "Scelta non valida, i valori supportati sono\n");
    fprintf(stderr, "1: Eulero\n2: Eulero-Cromer\n3: Velocity Verlet\n");
    exit(1);
}

Il nome di una funzione, quando viene utilizzato senza parentesi, rappresenta il suo indirizzo. Scrivere

passo = eulero;

fa quindi puntare passo alla funzione eulero, ma non esegue ancora la funzione. Per eseguirla usiamo invece le parentesi, esattamente come per una normale chiamata:

passo(&sist, &punto);

Il ciclo che realizza la simulazione non deve più sapere quale algoritmo sia stato scelto:

double t = 0.0;
Sistema sistema;
Punto punto;

/* inizializzazione */

while(t < t_max) {
    printf("%g %g %g\n", t, punto.x, punto.v);
    passo(&sistema, &punto);
    t += sistema.dt;
}

A ogni iterazione passo chiamerà eulero, eulero_cromer oppure velocity_verlet, a seconda della scelta iniziale. La decisione viene quindi presa a runtime, mentre il programma è in esecuzione.

Il vantaggio non consiste soltanto nel risparmiare qualche riga. Tutto ciò che non dipende dall’algoritmo, come condizioni iniziali, ciclo temporale, scrittura dei risultati e calcolo dell’errore, rimane in un unico punto del programma. Per aggiungere un nuovo integratore sarà sufficiente scrivere una funzione con la stessa firma e assegnarne l’indirizzo a passo.

Un puntatore a funzione svolge dunque, per le funzioni, un ruolo simile a quello che un normale puntatore svolge per i dati: permette di memorizzare e passare ad altre parti del programma non un valore su cui operare, ma l’operazione da eseguire.

5Il moto del pendolo semplice

Torniamo ora al pendolo semplice introdotto nell’eq. (5). A differenza dell’oscillatore armonico, il pendolo è un sistema non lineare e rappresenta quindi un banco di prova più realistico per gli algoritmi di integrazione numerica discussi finora. Introducendo la velocità angolare ω(t)θ˙(t)\omega(t) \equiv \dot{\theta}(t), l’equazione del moto può essere riscritta come il sistema di due equazioni del primo ordine

{dθdt=ω(t),dωdt=gLsinθ(t).\begin{cases} \od{\theta}{t} = \omega(t),\\ \od{\omega}{t} = -\dfrac{g}{L}\sin\theta(t). \end{cases}

In assenza di attrito l’energia meccanica

E(t)=12mL2ω2(t)+mgL[1cosθ(t)]E(t) = \frac{1}{2}mL^2\omega^2(t) + mgL\left[1-\cos\theta(t)\right]

è una costante del moto. Possiamo quindi utilizzare la conservazione dell’energia come ulteriore strumento per confrontare la qualità dei diversi algoritmi.

<Figure size 700x600 with 3 Axes>

Figure 8:Dall’alto verso il basso, i tre pannelli mostrano la posizione x(t)x(t), la velocità y(t)y(t) e l’energia meccanica E(t)E(t) in funzione del tempo per il pendolo semplice simulato utilizzando i metodi di Eulero, Eulero-Cromer e Velocity Verlet con parametri g=9.81g = 9.81 m/s2^2, L=10L = 10 m, m=1m = 1 Kg, θ0=0.5\theta_0 = 0.5 rad, dθdtt=t0=ω0=0.5\left.\od{\theta}{t}\right|_{t=t_0} = \omega_0 = 0.5 rad/s e passo di integrazione Δt=0.01\Delta t = 0.01 s.

La Figura 8 mostra una differenza qualitativa già osservata nel caso dell’oscillatore armonico. Il metodo di Eulero introduce una deriva sistematica: come abbiamo visto, a ogni oscillazione il pendolo acquista una piccola quantità di energia numerica e la traiettoria si allontana progressivamente da quella fisica.

Diversamente, ed esattamente come per l’oscillatore armonico, per Eulero-Cromer e Velocity Verletl’energia numerica non è esattamente costante, ma compie piccole oscillazioni attorno al valore corretto senza mostrare una deriva sistematica (nella figura questo si vede solo per Eulero-Cromer). Come già discusso, questa proprietà è legata alla simpletticità dei due algoritmi: la discretizzazione modifica leggermente la dinamica del sistema, ma ne preserva la struttura geometrica nello spazio delle fasi. In effetti, per un integratore simplettico di ordine pp ci aspettiamo genericamente un andamento del tipo[13]

EnE0=ΔtpF(tn)+O(Δtp+1),E_n-E_0 = \Delta t^p F(t_n) + \mathcal{O}(\Delta t^{p+1}),

dove F(t)F(t) è una funzione limitata. L’ampiezza delle oscillazioni dell’energia deve quindi scalare come

δEΔtp.\delta E \propto \Delta t^p.

Possiamo verificare numericamente questa previsione. Una possibile misura dell’errore energetico è il massimo scostamento dal valore iniziale,

ϵEmaxtE(t)E0.\epsilon_E \equiv \max_{t} |E(t)-E_0|.

In alternativa possiamo considerare le fluttuazioni dell’energia attorno al suo valor medio,

σE=E2E2.\sigma_E = \sqrt{ \langle E^2\rangle - \langle E\rangle^2 }.

Le due quantità non sono identiche: ϵE\epsilon_E è sensibile anche a un eventuale spostamento del valor medio rispetto a E0E_0, mentre σE\sigma_E misura soltanto le fluttuazioni attorno alla media. Tuttavia, quando l’errore energetico è limitato e oscillatorio e la forma delle oscillazioni non cambia qualitativamente al variare di Δt\Delta t, entrambe presentano lo stesso comportamento asintotico,

ϵE,σEΔtp.\epsilon_E,\sigma_E \propto \Delta t^p.
<Figure size 700x600 with 2 Axes>

Figure 9:Massimo scostamento dell’energia dal valore iniziale, ϵE\epsilon_E (in alto) e fluttuazioni dell’energia attorno al valor medio, σE\sigma_E (in basso), in funzione del passo temporale Δt\Delta t per il pendolo semplice. Le due rette continue indicano le dipendenze Δt\propto\Delta t e Δt2\propto\Delta t^2. I parametri delle simulazioni sono gli stessi della Figura 8.

I risultati della Figura 9 sono coerenti con quanto ci aspettiamo dall’ordine dei metodi. Eulero-Cromer è un algoritmo simplettico del primo ordine e le oscillazioni dell’energia scalano quindi come Δt\Delta t. Velocity Verlet è invece simplettico e del secondo ordine, e infatti le oscillazioni dell’energia scalano come Δt2\Delta t^2.

Il metodo di Eulero mostra anch’esso, a tempo finale fissato e nel regime di piccoli Δt\Delta t, un errore energetico che diminuisce linearmente con Δt\Delta t. Questo fatto, però, non implica che Eulero sia simplettico: la differenza fondamentale è il comportamento temporale dell’errore. Nel caso di Eulero l’errore tende ad accumularsi producendo una deriva dell’energia, mentre per Eulero-Cromer e Velocity Verlet esso rimane limitato e oscillatorio. La sola dipendenza di ϵE\epsilon_E da Δt\Delta t non è quindi sufficiente a stabilire se un algoritmo sia simplettico. La caratteristica distintiva dei metodi simplettici è la combinazione delle due proprietà: l’ampiezza dell’errore energetico diminuisce con l’ordine previsto e, soprattutto, non presenta una deriva secolare a tempi lunghi.

6Runge-Kutta

Molti dei problemi complessi da risolvere con metodi numerici non riguardano sistemi in cui l’energia si conserva. La simpletticità non è quindi sempre un requisito necessario. Vediamo subito un esempio.

6.1L’oscillatore armonico smorzato

Un oggetto che si muove lentamente in un fluido viscoso è sottoposto, sotto opportune condizioni, a una forza di attrito proporzionale e opposta alla sua velocità[14]. Nel caso di un oscillatore armonico, la dinamica del sistema è descritta dalla seguente equazione differenziale:

x(t)=ω02x(t)γmx(t),x''(t) = -\omega_0^2 x(t) - \frac{\gamma}{m} x'(t),

dove γ0\gamma \geq 0 è il coefficiente di attrito, che determina l’intensità della dissipazione di energia. Introducendo la quantità

ω2=ω02γ24m2,\omega^2 = \omega_0^2 - \frac{\gamma^2}{4m^2},

si possono distinguere tre diversi regimi dinamici, a seconda del segno di ω2\omega^2.

  1. ω2>0\omega^2>0: smorzamento sottocritico. La soluzione può essere scritta nella forma

    x(t)=Ceγ2mtcos(ωt+ϕ),x(t)=C e^{-\frac{\gamma}{2m}t}\cos(\omega t+\phi),

    dove CC e ϕ\phi sono costanti determinate dalle condizioni iniziali. Il sistema oscilla con pulsazione ω\omega, mentre l’ampiezza delle oscillazioni si riduce esponenzialmente nel tempo.

  2. ω2=0\omega^2=0: smorzamento critico. La soluzione ha la forma

    x(t)=(c1+c2t)eγ2mt.x(t)=(c_1+c_2t)e^{-\frac{\gamma}{2m}t}.

    Il sistema non oscilla e ritorna all’equilibrio nel minor tempo possibile senza oltrepassarlo.

  3. ω2<0\omega^2<0: smorzamento sovracritico. Ponendo

    Ω=γ24m2ω02,\Omega=\sqrt{\frac{\gamma^2}{4m^2}-\omega_0^2},

    la soluzione può essere scritta come una combinazione di due esponenziali decrescenti e non presenta oscillazioni:

    x(t)=c1e(γ2m+Ω)t+c2e(γ2mΩ)t.x(t)=c_1e^{\left(-\frac{\gamma}{2m}+\Omega\right)t} +c_2e^{\left(-\frac{\gamma}{2m}-\Omega\right)t}.

Per γ>0\gamma>0, in tutti e tre i regimi si ha x(t)0x(t)\to 0 e x(t)=v(t)0x'(t) = v(t) \to 0 per tt\to\infty. L’energia meccanica

E(t)=12m[v(t)]2+12mω02[x(t)]2E(t)=\frac{1}{2}m[v(t)]^2+\frac{1}{2}m\omega_0^2[x(t)]^2

decresce infatti secondo

dEdt=γ[v(t)]20,\od{E}{t}=-\gamma[v(t)]^2\leq 0,

e tende a zero a tempi lunghi.

<Figure size 640x480 with 1 Axes>

Figure 10:Come in Figura 7 per per l’oscillatore armonico smorzato. Il tempo totale di simulazione è tf=20t_f = 20, mentre i parametri utilizzati sono k=1k = 1, m=1m = 1 (e quindi ω0=1\omega_0 = 1), γ=0.5\gamma = 0.5, x0=2x_0 = 2, v0=1v_0 = 1.

La Figura 10 mostra l’errore globale che i tre algoritmi che abbiamo studiato compiono nel caso di un oscillatore armonico smorzato. Qualitativamente, i risultati sono simili a quelli ottenuti per l’oscillatore armonico non smorzato e mostrati nella Figura 7, con una differenza importante: il metodo di Velocity Verlet, pur risultando quantitativamente più accurato dei metodi di Eulero e di Eulero-Cromer, presenta in questo caso un errore globale che scala come O(Δt)\mathcal{O}(\Delta t).

Questa perdita di accuratezza è dovuta al fatto che la formulazione standard del Velocity Verlet è costruita per sistemi nei quali l’accelerazione dipende dalla posizione, ma non dalla velocità. Nell’oscillatore smorzato, invece,

a(x,v)=ω02x(t)γmv(t),a(x,v)=-\omega_0^2x(t) - \frac{\gamma}{m}v(t),

e il calcolo dell’accelerazione al passo successivo richiede quindi anche una stima della nuova velocità. Se il metodo viene applicato senza modificarne la struttura per trattare esplicitamente questa dipendenza, l’accelerazione viene valutata utilizzando una velocità non ancora aggiornata in modo pienamente consistente. L’errore introdotto da questa approssimazione è di ordine O(Δt2)\mathcal{O}(\Delta t^2) per ogni singolo passo e si accumula nel corso dell’integrazione, producendo un errore globale di ordine O(Δt)\mathcal{O}(\Delta t). Il Velocity Verlet standard perde pertanto, in presenza di forze dipendenti dalla velocità, la convergenza del secondo ordine che possiede per sistemi conservativi con accelerazione dipendente dalla sola posizione.

Per ovviare a questo problema si può estendere il metodo Velocity Verlet al caso in cui la forza dipenda esplicitamente dalla velocità, oppure utilizzare un metodo non simplettico che non soffre di questi problemi. In questa sezione presenteremo il più famoso di questi metodi, noto come Runge-Kutta, discutendo la sua implementazione al secondo (RK2) e al quarto (RK4) ordine. Per rendere la trattazione della stabilità e dell’accuratezza direttamente confrontabile con quella dei capitoli precedenti, presenteremo inizialmente i nuovi metodi utilizzando l’oscillatore armonico non smorzato, eq. (29). L’oscillatore armonico smorzato sarà invece il sistema modello che utilizzeremo alla fine della sezione per confrontare gli algoritmi già esaminati con i due RK che introdurremo qui.

6.2Metodo Runge-Kutta del secondo ordine (RK2)

I metodi introdotti finora approssimano il valore medio della derivata nell’intervallo [tn,tn+1][t_n,t_{n+1}] utilizzando informazioni disponibili agli estremi dell’intervallo stesso. Possiamo però ottenere una stima più accurata osservando che, per una funzione sufficientemente regolare, il valore della derivata nel punto medio dell’intervallo costituisce spesso una buona approssimazione del suo valore medio.

L’idea alla base del metodo Runge-Kutta del secondo ordine consiste quindi nello stimare la derivata nel punto medio dell’intervallo e utilizzarla per aggiornare la soluzione. Partendo dal valore noto xnx_n, si esegue innanzitutto un mezzo passo con il metodo di Eulero:

{xn=f(xn,tn)xn+12=xn+xnΔt2.\begin{cases} x'_n = f(x_n,t_n)\\ x_{n+\frac{1}{2}} = x_n + x'_n\frac{\Delta t}{2}. \end{cases}

Questa quantità fornisce una stima della soluzione al tempo tn+12=tn+Δt/2t_{n+\frac12} = t_n + \Delta t / 2. Possiamo quindi calcolare una nuova stima della derivata nel punto medio:

xn+1/2=f(xn+12,tn+12).x'_{n+1/2} = f\left(x_{n+\frac12}, t_{n+\frac12}\right).

Infine, utilizziamo questa derivata per avanzare di un passo completo:

xn+1=xn+xn+1/2Δt.x_{n+1} = x_n + x'_{n+1/2} \Delta t.

Ricordando che, nel nostro caso, x(t)=v(t)x'(t) = v(t), applicando il metodo al nostro sistema di equazioni otteniamo

{xn+1=xn+(vn+anΔt2)Δt=xn+vnΔt+12anΔt2vn+1=vn+an+1/2Δt,\begin{cases} x_{n+1} = x_n + \left(v_n + a_n \frac{\Delta t}{2}\right) \Delta t = x_n + v_n \Delta t + \frac{1}{2} a_n \Delta t^2\\ v_{n+1} = v_n + a_{n+1/2} \Delta t, \end{cases}

dove an+1/2=a(xn+1/2,vv+1/2,tn+1/2)a_{n+1/2} = a(x_{n+1/2}, v_{v+1/2}, t_{n+1/2}) è l’accelerazione calcolata nel punto medio dell’intervallo, ottenuta integrando di mezzo passo xnx_n e vnv_n. Calcolare due volte l’accelerazione (in nn e in n+1/2n + 1/2) è il prezzo computazionale che si paga per migliorare l’accuratezza rispetto al metodo di Eulero. Questo è un prezzo che molte volte (ma non necessariamente sempre) vale la pena di pagare.

6.2.1Stabilità ed accuratezza

Considerando che, per l’oscillatore armonico, an+1/2=ω0xn+1/2=ω0(xn+vnΔt/2)a_{n+1/2} = -\omega_0 x_{n+1/2} =-\omega_0(x_n + v_n \Delta t/2), le equazioni di aggiornamento (125) si possono scrivere come

{xn+1=xn+(vnω0xnΔt2)Δtvn+1=vn+(ω02xn12ω02Δtvn)Δt,\begin{cases} x_{n+1} = x_n + \left(v_n - \omega_0 x_n \frac{\Delta t}{2}\right) \Delta t\\ v_{n+1} = v_n + \left(-\omega_0^2 x_n - \frac{1}{2}\omega_0^2 \Delta t v_n\right) \Delta t, \end{cases}

che permette di scrivere la matrice di propagazione per il metodo di RK2:

M^RK2=(112ω02Δt2Δtω02Δt112ω02Δt2,)\hat{M}_{RK2} = \begin{pmatrix} 1 - \frac{1}{2} \omega_0^2 \Delta t^2 & \Delta t \\ -\omega_0^2 \Delta t & 1 - \frac{1}{2} \omega_0^2 \Delta t^2, \end{pmatrix}

il cui determinante vale

det(M^RK2)=(112ω02Δt2)2+ω02Δt2=1+14ω04Δt41.\det(\hat{M}_{RK2}) = \left(1 - \frac{1}{2} \omega_0^2 \Delta t^2\right)^2 + \omega_0^2 \Delta t^2 = 1 + \frac{1}{4} \omega_0^4 \Delta t^4 \geq 1.

Poiché il determinante della matrice è sempre maggiore di uno, il metodo non conserva l’energia e quindi non può essere simplettico, e le traiettorie generate nello spazio delle fasi spiraleggiano verso l’esterno. Come fatto in precedenza, utilizziamo il polinomio caratteristico per calcolare gli autovalori, che valgono

λ1,2=(112ω02Δt2)±iω0Δt.\lambda_{1,2} = \left(1 - \frac{1}{2} \omega_0^2 \Delta t^2\right) \pm i \omega_0 \Delta t.

I due autovalori sono sempre complessi coniugati e quindi hanno lo stesso modulo, che vale 1+14ω04Δt41\sqrt{1 + \frac{1}{4}\omega_0^4 \Delta t^4} \geq 1: il metodo è, come quello di Eulero, incondizionatamente instabile, ma in questo caso la differenza tra il modulo al quadrato degli autovalori e 1 è più piccola (14ω04Δt4\frac{1}{4}\omega_0^4 \Delta t^4 vs. ω02Δt2\omega_0^2 \Delta t^2)[15]. Di conseguenza, l’instabilità diventa evidente dopo tempi di integrazione molto più lunghi rispetto ad Eulero.

Per quanto riguarda l’accuratezza, notiamo prima di tutto che l’aggiornamento delle posizioni, cioè la prima delle equazioni (125), è lo stesso del metodo di Velocity Verlet, eq. (92). Di conseguenza, i due algoritmi condividono l’errore di troncamento locale per le posizioni, che va come O(Δt3)\mathcal{O}(\Delta t^3). Per le velocità applichiamo lo stesso procedimento visto per Velocity Verlet espandendo an+1/2a_{n+1/2} per ottenere

vn+1=vn+a(tn)Δt+12da(tn)dtΔt2+O(Δt3).v_{n+1} = v_n + a(t_n) \Delta t + \frac{1}{2} \od{a(t_n)}{t} \Delta t^2 + \mathcal{O}(\Delta t^3).

Se ora sottraiamo questa quantità da quella teorica, eq. (99), e assumiamo come al solito che al tempo tnt_n lo stato numerico coincida con quello esatto (xn=x(tn)x_n = x(t_n), vn=v(tn)v_n = v(t_n), an=a(tn)a_n = a(t_n)), otteniamo:

vn+1v(tn+Δt)=16d2a(tn)dt2Δt3+O(Δt4),v_{n+1} - v(t_n + \Delta t) = -\frac{1}{6} \odd{a(t_n)}{t} \Delta t^3 + \mathcal{O}(\Delta t^4),

e quindi anche per la velocità l’errore locale dell’algoritmo RK2 è di ordine O(Δt3)\mathcal{O}(\Delta t^3).

Quindi, sia per le posizioni che per le velocità l’errore globale scala come O(Δt2)\mathcal{O}(\Delta t^2): RK2 è un algoritmo del secondo ordine nel tempo. La figura che mostra questo andamento è mostrata e discussa più sotto.

6.3Metodo Runge-Kutta del quarto ordine (RK4)

Il metodo RK2 migliora l’accuratezza dell’integrazione utilizzando una stima della derivata nel punto medio dell’intervallo. Possiamo però ottenere una stima ancora più accurata del valore medio della derivata combinando informazioni provenienti da più punti dell’intervallo stesso.

L’idea alla base del metodo Runge-Kutta del quarto ordine consiste nel costruire una successione di stime della derivata e combinarle opportunamente per ottenere una migliore approssimazione del valore medio di f(x,t)f(x,t) tra tnt_n e tn+1t_{n+1}.

Si definiscono innanzitutto quattro stime della derivata:

k1=f(xn,tn),k_1 = f(x_n,t_n),

che rappresenta la derivata all’inizio dell’intervallo,

k2=f(xn+k1Δt2,tn+1/2),k_2 = f \left(x_n+k_1\frac{\Delta t}{2}, t_{n+1/2}\right),

che fornisce una prima stima della derivata nel punto medio,

k3=f(xn+k2Δt2,tn+1/2),k_3 = f \left(x_n+k_2 \frac{\Delta t}{2}, t_{n+1/2}\right),

che costituisce una stima migliorata della derivata nel punto medio,

e infine

k4=f(xn+k3Δt,tn+1),k_4 = f\left(x_n+k_3 \Delta t ,t_{n+1}\right),

che rappresenta una stima della derivata alla fine dell’intervallo.

Queste quattro quantità, pesate opportunamente, si possono combinare per ottenere una stima della derivata media:

fni=14biki=b1k1+b2k2+b3k3+b4k4,\langle f \rangle_n \approx \sum_{i=1}^4 b_i k_i = b_1 k_1 + b_2 k_2 + b_3 k_3 + b_4 k_4,

dove i bib_i vanno scelti in modo da minimizzare l’errore. Si può dimostrare (vedi box sotto per una derivazione semplificata) che fissando b1=b4=1/6b_1 = b_4 = 1/6 e b2=b3=1/3b_2 = b_3 = 1/3 il metodo fornisce una stima particolarmente accurata della derivata media nell’intervallo, raggiungendo un’accuratezza molto superiore rispetto a Eulero ed RK2, al costo di quattro valutazioni della funzione ff per ogni passo temporale.

L’aggiornamento della soluzione assume pertanto la forma

xn+1=xn+Δt6(k1+2k2+2k3+k4).x_{n+1} = x_n + \frac{\Delta t}{6} (k_1 + 2k_2 + 2k_3 + k_4).

Se siamo interessato a un sistema dinamico unidimensionale come l’oscillatore armonico, il metodo RK4 va applicato simultaneamente alle due variabili xx e vv. In altre parole, a ogni passo temporale dobbiamo costruire quattro stime sia per la derivata della posizione, cioè la velocità, sia per la derivata della velocità, cioè l’accelerazione.

Partendo dallo stato noto (xn,vn)(x_n,v_n) al tempo tnt_n, definiamo innanzitutto

{k1,x=vnk1,v=a(xn,vn,tn).\begin{cases} k_{1,x} = v_n\\ k_{1,v} = a(x_n,v_n,t_n). \end{cases}

Queste sono le derivate valutate all’inizio dell’intervallo. Usiamo poi queste quantità per stimare lo stato del sistema a metà passo:

{xn+12(1)=xn+k1,xΔt2vn+12(1)=vn+k1,vΔt2,\begin{cases} x_{n+\frac12}^{(1)} = x_n + k_{1,x}\frac{\Delta t}{2}\\ v_{n+\frac12}^{(1)} = v_n + k_{1,v}\frac{\Delta t}{2}, \end{cases}

e calcoliamo le derivate in questo punto intermedio:

{k2,x=vn+12(1)k2,v=a(xn+12(1),vn+12(1),tn+Δt2).\begin{cases} k_{2,x} = v_{n+\frac12}^{(1)}\\ k_{2,v} = a\left(x_{n+\frac12}^{(1)},v_{n+\frac12}^{(1)},t_n+\frac{\Delta t}{2}\right). \end{cases}

Ripetiamo ora la stessa procedura, ma usando k2k_2 per ottenere una stima migliorata dello stato a metà passo:

{xn+12(2)=xn+k2,xΔt2vn+12(2)=vn+k2,vΔt2,\begin{cases} x_{n+\frac12}^{(2)} = x_n + k_{2,x}\frac{\Delta t}{2}\\ v_{n+\frac12}^{(2)} = v_n + k_{2,v}\frac{\Delta t}{2}, \end{cases}

da cui

{k3,x=vn+12(2)k3,v=a(xn+12(2),vn+12(2),tn+Δt2).\begin{cases} k_{3,x} = v_{n+\frac12}^{(2)}\\ k_{3,v} = a\left(x_{n+\frac12}^{(2)},v_{n+\frac12}^{(2)},t_n+\frac{\Delta t}{2}\right). \end{cases}

Infine, usiamo k3k_3 per stimare lo stato alla fine dell’intervallo:

{xn+1(3)=xn+k3,xΔtvn+1(3)=vn+k3,vΔt,\begin{cases} x_{n+1}^{(3)} = x_n + k_{3,x}\Delta t\\ v_{n+1}^{(3)} = v_n + k_{3,v}\Delta t, \end{cases}

e calcoliamo l’ultima coppia di derivate:

{k4,x=vn+1(3)k4,v=a(xn+1(3),vn+1(3),tn+Δt).\begin{cases} k_{4,x} = v_{n+1}^{(3)}\\ k_{4,v} = a\left(x_{n+1}^{(3)},v_{n+1}^{(3)},t_n+\Delta t\right). \end{cases}

L’aggiornamento completo si ottiene quindi combinando le quattro stime con gli stessi pesi già ricavati per il caso generale:

{xn+1=xn+Δt6(k1,x+2k2,x+2k3,x+k4,x)vn+1=vn+Δt6(k1,v+2k2,v+2k3,v+k4,v).\begin{cases} x_{n+1} = x_n + \frac{\Delta t}{6} \left(k_{1,x}+2k_{2,x}+2k_{3,x}+k_{4,x}\right)\\ v_{n+1} = v_n + \frac{\Delta t}{6} \left(k_{1,v}+2k_{2,v}+2k_{3,v}+k_{4,v}\right). \end{cases}

Nota Bene: per l’oscillatore armonico l’accelerazione dipende solo dalla velocità,e quindi le quantità ki,vk_{i,v} si ottengono semplicemente valutando ω02x-\omega_0^2 x nei diversi punti intermedi costruiti dall’algoritmo. Nel caso più generale (ad esempio quello dell’oscillatore smorzato o forzato), l’accelerazione dipende anche da vv e da tt, e quindi è importante aggiornare correttamente entrambe le variabili nei passi intermedi.

6.3.1Stabilità ed accuratezza

Come per gli algoritmi visti, studiamo la stabilità di RK4 applicandolo all’oscillatore armonico. Poiché in questo caso i calcoli sono piuttosto lunghi e non fondamentali per quello che ci interessa, li riporto in un box più in basso. Discutiamo invece i risultati principali:

Il determinante della matrice di propagazione è

det(M^RK4)=1ω06Δt672+z6576.\det(\hat{M}_{RK4}) = 1 - \frac{\omega_0^6\Delta t^6}{72} + \frac{z^6}{576}.

Gli autovalori invece valgono

λ1,2=1ω02Δt22+ω04Δt424±i(ω0Δtω03Δt36).\lambda_{1,2} = 1 - \frac{\omega_0^2\Delta t^2}{2} + \frac{\omega_0^4\Delta t^4}{24} \pm i\left(\omega_0\Delta t - \frac{\omega_0^3\Delta t^3}{6}\right).

Poiché gli autovalori sono complessi coniugati, il loro modulo è uguale al determinante:

λ12=λ22=det(M^RK4)=1ω06Δt672+ω08Δt8576.|\lambda_1|^2 = |\lambda_2|^2 = \det(\hat{M}_{RK4}) = 1 - \frac{\omega_0^6\Delta t^6}{72} + \frac{\omega_0^8\Delta t^8}{576}.

Questo risultato mostra che, come RK2, anche RK4 non è simplettico. Tuttavia la deviazione da 1 compare soltanto a partire dall’ordine z6=(ω0Δt)6z^6 = (\omega_0\Delta t)^6, cioè è molto piccola per passi temporali sufficientemente piccoli. A differenza di RK2, l’espressione del determinante ha un termine negativo e uno positivo, e quindi può cambiare segno a seconda dei parametri. La condizione per cui la dinamica non esplode è det(M^RK4)1\det(\hat{M}_{RK4}) \leq 1, cioè

ω06Δt6(172+ω02Δt2576)0,\omega_0^6\Delta t^6\left(-\frac{1}{72} + \frac{\omega_0^2\Delta t^2}{576} \right) \leq 0,

che, per ω0Δt>0\omega_0\Delta t > 0, equivale a

172+ω02Δt25760,-\frac{1}{72} + \frac{\omega_0^2\Delta t^2}{576} \leq 0,

da cui otteniamo la condizione di stabilità

ω0Δt22,    Δt22ω0.\omega_0\Delta t \leq 2\sqrt{2}, \implies \Delta t \leq \frac{2\sqrt{2}}{\omega_0}.

Il metodo RK4 è dunque condizionatamente stabile per l’oscillatore armonico. Il limite di stabilità è meno restrittivo di quello trovato per Eulero-Cromer e Velocity Verlet, eq. (78), ma questo non significa che RK4 sia sempre preferibile. Infatti, RK4 non è simplettico: anche quando è stabile, non conserva esattamente l’area nello spazio delle fasi. Per z<22z < 2\sqrt{2} il determinante è leggermente minore di 1, quindi lo schema introduce una piccola dissipazione numerica: le orbite nello spazio delle fasi tendono a spiraleggiare molto lentamente verso l’interno. Per z>22z > 2\sqrt{2}, invece, il determinante (e quindi il modulo di entrambi gli autovalori) diventa maggiore di 1 e la soluzione numerica diverge.

Discutiamo ora l’accuratezza dell’algoritmo. Come viene dimostrato formalmente nel box sotto, ma si può anche inferire notando che i coefficienti numerici sono scelti in maniera da eguagliare al quarto ordine lo sviluppo di Taylor della soluzione analitica, l’errore di troncamento locale è O(Δt5)\mathcal{O}(\Delta t^5). L’errore globale accumulato scala quindi come

1ΔtO(Δt5)=O(Δt4).\frac{1}{\Delta t}\mathcal{O}(\Delta t^5) = \mathcal{O}(\Delta t^4).
<Figure size 640x480 with 1 Axes>

Figure 11:Come in Figura 6, con, in aggiunta, l’errore ottenuto applicando gli algoritmi Runge-Kutta del secondo e quarto ordine.

La figura Figura 11 mostra l’andamento degli errori globali di RK2 ed RK4 insieme a quelli degli altri algoritmi visti finora in questa sezione. È evidente che l’errore di RK4 diminuisce molto più rapidamente di quello di RK2 o, equivalentemente, Velocity Verlet, quando si riduce il passo temporale. Il grafico mostra addirittura una saturazione per piccoli valori di Δt\Delta t: per questi valori del passo di integrazione, l’errore dovuto alla precisione numerica delle operazioni in virgola mobile comincia a dominare l’errore totale, e l’errore di troncamento diventa trascurabile.

Riassumendo, RK4 è molto accurato su intervalli di tempo finiti, ma non rispetta esattamente la struttura geometrica dei sistemi conservativi, e la mancata simpletticità può produrre una lenta deriva artificiale dell’energia. Per simulazioni molto lunghe di sistemi conservativi, un metodo simplettico come Velocity Verlet può quindi produrre un comportamento qualitativamente migliore, anche se l’ordine formale di accuratezza è più basso.

6.4Confronto tra algoritmi: l’oscillatore armonico smorzato

<Figure size 640x480 with 1 Axes>

Figure 12:Come in Figura 11 per l’oscillatore armonico smorzato. Il tempo totale di simulazione è tf=20t_f = 20, mentre i parametri utilizzati sono k=1k = 1, m=1m = 1 (e quindi ω0=1\omega_0 = 1), γ=0.5\gamma = 0.5, x0=2x_0 = 2, v0=1v_0 = 1.

La Figura 12 mostra l’andamento degli errori globali ottenuti risolvendo numericamente l’equazione dell’oscillatore armonico smorzato, eq. (113), con i diversi metodi di integrazione introdotti in questa sezione. Si vede bene il comportamento di scala dell’errore globale degli algoritmi RK2 e RK4 rimanga invariato anche in sistemi in cui, come in questo caso, la forza dipende esplicitamente dalla velocità. Questo fa sì che RK4 (che è marginalmente più complesso da implementare e, per un calcolatore, da eseguire) sia il metodo più comune per risolvere equazioni (o sistemi di equazioni) differenziali che non richiedono la proprietà di simpletticità.

7Dal moto unidimensionale ai sistemi planetari

Finora abbiamo applicato gli algoritmi di integrazione numerica a sistemi con un solo grado di libertà, descritti da una posizione x(t)x(t) e da una velocità v(t)v(t). Gli stessi metodi possono però essere utilizzati per studiare sistemi multidimensionali formati da molti corpi. In questo caso lo stato del sistema non è più rappresentato da una singola coppia (x,v)(x,v), ma dall’insieme delle posizioni e delle velocità di tutti i corpi.

Come esempio consideriamo un sistema formato da una stella di massa MM, mantenuta fissa nell’origine, e da NN pianeti vincolati a muoversi sullo stesso piano. Il pianeta ii-esimo ha massa mim_i, posizione

ri(t)=(xi(t)yi(t))\mathbf r_i(t)= \begin{pmatrix} x_i(t)\\ y_i(t) \end{pmatrix}

e velocità

vi(t)=(vx,i(t)vy,i(t)).\mathbf v_i(t)= \begin{pmatrix} v_{x,i}(t)\\ v_{y,i}(t) \end{pmatrix}.

L’ipotesi di moto planare è appropriata quando le posizioni e le velocità iniziali appartengono allo stesso piano. Le forze gravitazionali rimangono infatti contenute in quel piano e non possono generare una componente del moto perpendicolare a esso.

Mantenere la stella immobile è invece un’approssimazione: anche la stella dovrebbe accelerare per effetto dell’attrazione dei pianeti. L’approssimazione è ragionevole quando la massa della stella è molto maggiore della somma delle masse planetarie, o in altre parole

imiM1,\frac{\sum_i m_i}{M} \ll 1,

così che il suo spostamento risulti molto piccolo rispetto alle dimensioni delle orbite. Ad esempio, per il sistema solare la somma delle masse dei pianeti è 2.6675×1027\approx 2.6675 \times 10^{27} kg (con Giove che da solo rappresenta più del 70% della massa planetaria), mentre la massa del Sole vale M1.9891×1030M_\odot \approx 1.9891 \times 10^{30} kg. Quindi, se volessimo simulare il nostro sistema solare, dove la massa planetaria è appena lo 0.134% di quella solare, la relazione (197) sarebbe valida.

7.1Pianeti non interagenti

Consideriamo inizialmente pianeti che interagiscono con la stella, ma non tra loro. La forza gravitazionale esercitata dalla stella sul pianeta ii è

Fi=GMmiri3ri,\mathbf F_i = -\frac{GMm_i}{|\mathbf r_i|^3}\mathbf r_i,

dove GG è la costante di gravitazione universale e

ri=xi2+yi2|\mathbf r_i|=\sqrt{x_i^2+y_i^2}

è la distanza del pianeta dalla stella. Applicando la seconda legge di Newton si ottiene

ai=Fimi=GMri3ri.\mathbf a_i = \frac{\mathbf F_i}{m_i} = - \frac{GM}{|\mathbf r_i|^3}\mathbf r_i.

La massa del pianeta non compare nell’accelerazione: a parità di posizione e velocità iniziali, tutti i corpi seguono la stessa traiettoria indipendentemente dalla loro massa.

In componenti, le equazioni del moto sono

{xi=vx,i,yi=vy,i,vx,i=GMxi(xi2+yi2)3/2,vy,i=GMyi(xi2+yi2)3/2.\begin{cases} x'_i=v_{x,i},\\ y'_i=v_{y,i},\\ v'_{x,i}=-\dfrac{GMx_i}{(x_i^2+y_i^2)^{3/2}},\\ v'_{y,i}=-\dfrac{GMy_i}{(x_i^2+y_i^2)^{3/2}}. \end{cases}

Per ogni pianeta dobbiamo quindi integrare quattro equazioni differenziali del primo ordine. Un sistema di NN pianeti è descritto complessivamente da 4N4N variabili dinamiche. Nel caso non interagente, tuttavia, le equazioni relative a pianeti diversi sono indipendenti: stiamo semplicemente risolvendo NN problemi di Keplero separati.

7.2Orbite circolari

Un caso particolarmente semplice è quello di un pianeta in orbita circolare di raggio RR. In questo caso l’accelerazione gravitazionale deve coincidere con l’accelerazione centripeta:

v2R=GMR2.\frac{v^2}{R}=\frac{GM}{R^2}.

La velocità necessaria per ottenere un’orbita circolare è quindi

vcirc=GMR.v_{\rm circ}=\sqrt{\frac{GM}{R}}.

Se inizialmente il pianeta si trova nel punto (R,0)(R,0), una possibile condizione iniziale è pertanto

r(0)=(R,0),v(0)=(0,GMR).\mathbf r(0)=(R,0), \qquad \mathbf v(0)=\left(0,\sqrt{\frac{GM}{R}}\right).

Il periodo dell’orbita vale

T=2πRvcirc=2πR3GM,T=\frac{2\pi R}{v_{\rm circ}} = 2\pi\sqrt{\frac{R^3}{GM}},

da cui segue la terza legge di Keplero,

T2=4π2GMR3.T^2=\frac{4\pi^2}{GM}R^3.

Le orbite circolari sono particolarmente utili per verificare un programma: raggio, velocità ed energia devono rimanere costanti, a meno dei piccoli errori introdotti dall’integrazione numerica.

7.3Energia e momento angolare

Poiché la forza gravitazionale è conservativa, a ciascun pianeta possiamo associare l’energia meccanica

Ei=12mivi2GMmiri.E_i = \frac{1}{2}m_i|\mathbf v_i|^2 - \frac{GMm_i}{|\mathbf r_i|}.

In assenza di interazioni fra i pianeti, l’energia di ciascun pianeta si conserva separatamente. Si conserva quindi anche l’energia totale

E=i=1NEi.E=\sum_{i=1}^N E_i.

Per un’orbita circolare, sostituendo vcirc2=GM/Rv_{\rm circ}^2=GM/R, si ottiene

Ei=GMmi2R.E_i=-\frac{GMm_i}{2R}.

L’energia è negativa perché il pianeta è gravitazionalmente legato alla stella. Più in generale, valori negativi dell’energia corrispondono a orbite legate, mentre un corpo con energia non negativa può allontanarsi indefinitamente dalla stella.

Il momento lineare dei pianeti non si conserva, perché la stella fissa esercita su di essi una forza esterna. Per descrivere un sistema completamente isolato sarebbe necessario lasciare libera anche la stella e integrare la sua equazione del moto. D’altro canto, la forza gravitazionale è una forza centrale: è sempre parallela a ri\mathbf r_i e non esercita quindi alcun momento rispetto all’origine. Di conseguenza, il momento angolare del pianeta si conserva. Nel moto planare l’unica componente non nulla è quella perpendicolare al piano:

Li=mi(ri×vi)z=mi(xivy,iyivx,i).L_i = m_i(\mathbf r_i\times\mathbf v_i)_z = m_i(x_i v_{y,i}-y_i v_{x,i}).

Nel caso non interagente si conserva separatamente ogni LiL_i, e di conseguenza anche

L=i=1NLi.L=\sum_{i=1}^N L_i.

Energia e momento angolare forniscono due strumenti fondamentali per valutare la qualità dell’integrazione numerica. Una deriva sistematica di queste quantità può segnalare un passo temporale troppo grande o un algoritmo poco adatto allo studio di sistemi conservativi.

7.4Interazioni fra i pianeti

In un sistema planetario reale ogni pianeta è attratto non soltanto dalla stella, ma anche dagli altri pianeti. La forza esercitata dal pianeta jj sul pianeta ii è

Fij=Gmimjrjrirjri3.\mathbf F_{ij} = Gm_i m_j \frac{\mathbf r_j-\mathbf r_i} {|\mathbf r_j-\mathbf r_i|^3}.

L’accelerazione complessiva del pianeta ii diventa quindi

ai=GMri3ri+GjiNmjrjrirjri3.\mathbf a_i = -\frac{GM}{|\mathbf r_i|^3}\mathbf r_i + G\sum_{\substack{j \ne i}}^N m_j \frac{\mathbf r_j-\mathbf r_i} {|\mathbf r_j-\mathbf r_i|^3}.

Le equazioni dei diversi pianeti sono ora accoppiate: per conoscere l’accelerazione di un pianeta dobbiamo conoscere simultaneamente le posizioni di tutti gli altri. In generale il problema non può più essere scomposto in orbite kepleriane indipendenti e non possiede una soluzione analitica.

L’interazione può produrre precessioni, scambi di energia e momento angolare, risonanze orbitali e, in opportune condizioni, dinamiche caotiche o espulsioni dal sistema. Anche quando le interazioni sono deboli, i loro effetti possono accumularsi su tempi molto lunghi e modificare sensibilmente le orbite.

L’energia meccanica totale deve ora includere anche l’energia potenziale associata a ogni coppia di pianeti:

E=i=1N[12mivi2GMmiri]i<jGmimjrirj.E = \sum_{i=1}^N \left[ \frac{1}{2}m_i|\mathbf v_i|^2 -\frac{GMm_i}{|\mathbf r_i|} \right] - \sum_{i<j} \frac{Gm_i m_j}{|\mathbf r_i-\mathbf r_j|}.

La condizione i<ji<j garantisce che ogni coppia venga contata una sola volta. Se sommassimo su tutti gli indici distinti iji\ne j, conteremmo infatti due volte la stessa interazione: una come coppia (i,j)(i,j) e una come coppia (j,i)(j,i).

In presenza di interazioni non si conserva più l’energia di ciascun pianeta: i pianeti possono scambiarsi energia. Si conserva invece l’energia totale del sistema. Analogamente, il momento angolare di ogni singolo pianeta può variare, mentre si conserva il momento angolare totale

L=i=1Nmi(xivy,iyivx,i).L = \sum_{i=1}^N m_i(x_i v_{y,i}-y_i v_{x,i}).

7.5Integrazione numerica di un sistema di molti corpi

Gli algoritmi introdotti in questo capitolo possono essere applicati senza modifiche concettuali. Posizioni e velocità non sono più singoli numeri, ma insiemi di array (o array di strutture). Poiché le forze gravitazionali dipendono dalle posizioni ma non dalle velocità, Velocity Verlet è una scelta naturale: è un metodo del secondo ordine e, essendo simplettico, descrive generalmente meglio l’evoluzione a lungo termine dei sistemi conservativi.

È però essenziale che, a ogni fase dell’algoritmo, le accelerazioni siano calcolate usando posizioni riferite allo stesso istante. Non possiamo aggiornare completamente il primo pianeta e utilizzare subito la sua nuova posizione per calcolare l’accelerazione del secondo: in questo modo il risultato dipenderebbe arbitrariamente dall’ordine con cui i pianeti sono memorizzati.

Un passo di Velocity Verlet deve quindi essere organizzato collettivamente:

  1. si calcolano le accelerazioni di tutti i pianeti;

  2. si aggiornano tutte le posizioni;

  3. si calcolano le nuove accelerazioni usando tutte le posizioni aggiornate;

  4. si aggiornano tutte le velocità.

Nel caso non interagente, il calcolo delle accelerazioni richiede un numero di operazioni proporzionale a NN. Includendo le interazioni, per ciascuno degli NN pianeti dobbiamo sommare il contributo degli altri N1N-1: il costo di un passo cresce quindi come N2N^2. Questa differenza diventa fondamentale nelle simulazioni con un numero molto grande di corpi.

Rimane infine un problema pratico. Finora il numero delle variabili era stabilito direttamente nel codice. In un programma generale per il moto planetario, invece, vogliamo scegliere il numero NN di pianeti durante l’esecuzione. Dobbiamo pertanto riservare una quantità di memoria che dipenda da un valore non noto al momento della compilazione. Nella prossima sezione vedremo come farlo in C mediante l’allocazione dinamica della memoria.

7.6C: Allocazione dinamica della memoria con malloc e free

Nel programma appena descritto vogliamo scegliere il numero NN di pianeti durante l’esecuzione, per esempio prendendolo come argomento dalla riga di comando. Non possiamo quindi dichiarare un array di dimensione fissata nel codice sorgente:

Pianeta pianeti[2];

Una dichiarazione di questo tipo permette di simulare esattamente due pianeti. Se volessimo modificarne il numero dovremmo cambiare il codice e compilarlo nuovamente.

Possiamo invece riservare la memoria necessaria durante l’esecuzione mediante la funzione malloc, dichiarata nell’header stdlib.h. Supponiamo che la struttura che rappresenta un pianeta sia stata definita nel modo seguente:

typedef struct {
    double m;
    double x, y;
    double vx, vy;
} Pianeta;

Dopo aver determinato il numero di pianeti, possiamo dichiarare un puntatore e allocare dinamicamente un array:

int N = atoi(argv[1]); /* primo argomento da riga di comando */

Pianeta *pianeti = malloc(N * sizeof(Pianeta));

La funzione malloc riceve come argomento il numero di byte da allocare. L’espressione sizeof(Pianeta) (o, equivalententemente, sizeof *pianeti), restituisce il numero di byte necessari per memorizzare un oggetto di tipo Pianeta. Il prodotto

N * sizeof(Pianeta)

è quindi la quantità di memoria necessaria per contenere NN pianeti, espressa in byte[16].

Il valore restituito da malloc è l’indirizzo iniziale della regione di memoria allocata. Possiamo quindi utilizzare pianeti come un normale array:

pianeti[0].m = 1.0e-3;
pianeti[0].x = 1.0;
pianeti[0].y = 0.0;
pianeti[0].vx = 0.0;
pianeti[0].vy = 1.0;

o, più in generale,

for (int i = 0; i < N; i++) {
    /* inizializzazione del pianeta i-esimo */
}

In generale, gli array dinamici sottostanno alle stesse regole degli array normali:

Notate inoltre che in C non è necessario convertire esplicitamente il valore restituito da malloc. È quindi possibile scrivere sia

Pianeta *pianeti = malloc(N * sizeof(Pianeta));

che

Pianeta *pianeti = (Pianeta *) malloc(N * sizeof(Pianeta));

La conversione da void *, il tipo restituito da malloc, a un altro tipo di puntatore avviene infatti automaticamente.

7.6.1Inizializzazione della memoria e calloc

malloc riserva la memoria, ma non ne inizializza il contenuto. Immediatamente dopo l’allocazione, i campi delle strutture contengono valori indeterminati:

Pianeta *pianeti = malloc(N * sizeof(Pianeta));

/* pianeti[0].x non possiede ancora un valore utilizzabile */

Prima di leggere un campo dobbiamo quindi assegnargli esplicitamente un valore. Nel programma sui pianeti questo avverrà naturalmente leggendo le masse e le condizioni iniziali dalla riga di comando o da un file. Se vogliamo invece che la memoria venga inizialmente azzerata, possiamo usare calloc al posto di malloc:

Pianeta *pianeti = calloc(N, sizeof(Pianeta));

A differenza di malloc, calloc riceve separatamente il numero di elementi e la dimensione di ciascun elemento, e inizializza a zero tutti i byte della memoria allocata. Per esempio, può essere comodo inizializzare a zero due array destinati a contenere le componenti delle accelerazioni:

double *ax = calloc(N, sizeof(double));
double *ay = calloc(N, sizeof(double));

Anche il risultato di calloc deve essere controllato:

if(ax == NULL || ay == NULL) {
    fprintf(stderr, "Impossibile allocare la memoria\n");
    exit(1);
}

D’altro canto, se dobbiamo comunque assegnare immediatamente un valore a tutti gli elementi, come nel caso delle condizioni iniziali dei pianeti, tanto vale usare malloc.

7.6.2Liberare la memoria con free

La memoria allocata dinamicamente rimane riservata finché non viene liberata esplicitamente. Quando un array non serve più, dobbiamo restituire la memoria al sistema mediante free:

free(pianeti);

Nel nostro programma, alla fine della simulazione scriveremo per esempio:

free(ax);
free(ay);
free(pianeti);

return 0;

Ogni regione ottenuta mediante malloc o calloc deve essere liberata esattamente una volta. Se perdiamo il puntatore senza aver chiamato free, la memoria rimane occupata ma non è più accessibile: si verifica quello che viene chiamato un memory leak, cioè una perdita di memoria.

Dopo free, il puntatore continua formalmente a contenere il vecchio indirizzo, ma la memoria corrispondente è stata “restituita” al sistema operativo, e quindi non appartiene più al programma. Non possiamo quindi usarlo:

free(pianeti);

/* Errore: la memoria è già stata liberata, probabile segmentation fault */
pianeti[0].x = 1.0;

Non bisogna neppure chiamare free due volte sullo stesso puntatore:

free(pianeti);
free(pianeti);  /* Errore */

7.6.3Il ciclo di vita dell’array

L’utilizzo della memoria dinamica segue quindi quattro passaggi:

  1. determiniamo durante l’esecuzione il numero di oggetti da memorizzare;

  2. allochiamo la memoria con malloc o calloc;

  3. controlliamo il risultato e utilizziamo l’array;

  4. liberiamo la memoria con free quando non serve più.

Nel caso dei pianeti, lo schema complessivo assume la forma

int N = atoi(argv[1]);

Pianeta *pianeti = malloc(N * sizeof(Pianeta));

if(pianeti == NULL) {
    fprintf(stderr, "Impossibile allocare la memoria\n");
    return 1;
}

/* Lettura delle condizioni iniziali */

/* Integrazione delle equazioni del moto */

/* Analisi o scrittura dei risultati */

free(pianeti);

return 0;

La memoria dinamica ci permette così di scrivere un solo programma capace di simulare un numero arbitrario di pianeti, limitato soltanto dalla memoria disponibile, senza conoscere NN quando scriviamo o compiliamo il codice.

Footnotes
  1. E non solo: le equazioni differenziali appaiono in praticamente ogni ambito scientifico, o comunque in cui analisi e modelli quantitativi sono possibili.

  2. La notazione a\vec{a} indica che aa è una quantità vettoriale, quindi (2) è un sistema di equazioni del secondo ordine.

  3. È possibile rendere questa definizione, che qui sembra piuttosto generica, formale e non ambigua.

  4. Che deriva dalla formula di bisezione cosx=12sin2x2\cos x = 1 - 2 \sin^2 \frac{x}{2}.

  5. Altri esempi di metodi comunemente utilizzati per risolvere sistemi di equazioni differenziali (spesso alle derivate parziali) sono gli elementi finiti (finite element methods) e la risoluzione in spazio di Fourier tramite Fast Fourier transform (FFT).

  6. Se avete un occhio attento potete notare qualche discrepanza tra la posizione teorica e quella ottenuta con Δt=101\Delta t = 10^{-1} in prossimità di massimi e minimi

  7. in effetti questo vale per qualunque sistema dinamico unidimensionale

  8. Oppure 1\to 1 se λ1=λ2\lambda_1 = \lambda_2, che non cambia il limite per mm \to \infty dell’equazione (58).

  9. In spazi delle fasi a più dimensioni (sistemi con N2N \ge 2 gradi di libertà, dove lo spazio delle fasi ha dimensione 2N42N \ge 4), la simpletticità è una condizione molto più restrittiva della semplice conservazione del volume. Un algoritmo simplettico deve conservare non solo il volume totale (det(M^)=1\det(\hat{M}) = 1), ma anche le proiezioni delle aree orientate su tutte le coppie di piani coordinati coniugati (xi,pi)(x_i, p_i), dove pip_i è il momento coniugato a xix_i.

  10. con quasi conservazione si intende quella proprietà per cui l’energia meccanica di un sistema fluttua intorno a un valore costante. Quando integriamo numericamente delle equazioni differenziali non possiamo sperare di fare meglio.

  11. Questo risultato si può ottenere immediatamente ricordando che det(M^)=λ1λ2\det(\hat{M}) = \lambda_1 \lambda_2.

  12. Si veda il box più in basso sul perché possiamo farlo.

  13. Questa proprietà deriva dal fatto che, per Δt\Delta t sufficientemente piccolo, una dinamica simplettica può essere interpretata come quella generata da un sistema con un’Hamiltoniana (non sapete cos’è? Lo saprete presto grazie al corso di Meccanica Analitica) leggermente modificata rispetto a quella originale,

    H~=H+O(Δtp).\widetilde{H} = H + \mathcal{O}(\Delta t^p).

    Questa proprietà, che può essere formalizzata tramite la cosiddetta backward error analysis, implica che l’errore sull’energia fisica rimanga generalmente limitato anche per tempi di integrazione molto lunghi.

  14. Il regime in cui la forza di attrito è proporzionale alla velocità si può quantificare introducendo il numero di Reynolds Re=ρvL/ηRe = \rho v L / \eta, dove ρ\rho è la densità del fluido, η\eta la sua viscosità dinamica, vv la velocità caratteristica e LL la dimensione caratteristica dell’oggetto. La legge di attrito lineare è valida per Re1Re \ll 1. Questa condizione si realizza tipicamente per oggetti molto piccoli, velocità ridotte o fluidi con elevata viscosità cinematica. A numeri di Reynolds elevati, in molti regimi il contributo dominante alla resistenza del fluido è invece approssimativamente proporzionale al quadrato della velocità.

  15. Se ω0Δt<1\omega_0 \Delta t < 1, come si dovrebbe sempre avere.

  16. Avremmo potuto scrivere anche malloc(N * sizeof *pianeti);. In questo caso sizeof avrebbe restituito il numero di byte necessari per memorizzare un oggetto del tipo a cui punta pianeti.