GNU Octave con GX-TXT — Lezione 04: condizioni, cicli e funzioni. Simuliamo la legge di raffreddamento di Newton
Nelle lezioni precedenti abbiamo imparato a utilizzare GNU Octave per eseguire calcoli, definire variabili e lavorare con vettori e matrici. Ora possiamo compiere un passo importante: scrivere programmi capaci di prendere decisioni, ripetere operazioni e organizzare i calcoli in funzioni riutilizzabili.
Questi tre concetti costituiscono le fondamenta della programmazione scientifica. Un programma, infatti, non deve necessariamente limitarsi a eseguire una sequenza fissa di istruzioni. Può verificare condizioni, scegliere comportamenti differenti, ripetere un calcolo fino al raggiungimento di un determinato risultato e suddividere problemi complessi in operazioni più semplici.
Per imparare a utilizzare queste strutture realizzeremo un piccolo esperimento numerico: simuleremo il raffreddamento di un corpo caldo immerso in un ambiente a temperatura costante, applicando la legge di raffreddamento di Newton.
Un esperimento di fisica per imparare a programmare
Immaginiamo di avere un corpo inizialmente alla temperatura di 80 °C, collocato in un ambiente mantenuto a 22 °C.
Sappiamo per esperienza che il corpo comincerà a raffreddarsi. Tuttavia, la sua temperatura non diminuirà con velocità costante: il raffreddamento sarà inizialmente più rapido e rallenterà progressivamente man mano che la temperatura del corpo si avvicina a quella dell’ambiente.
La legge di raffreddamento di Newton descrive questo comportamento mediante un’equazione differenziale relativamente semplice.
Il nostro obiettivo sarà costruire un programma che calcoli la temperatura del corpo a intervalli regolari, ricostruendone l’evoluzione durante un periodo di 60 minuti.
In particolare, utilizzeremo:
- Le condizioni
if,elseifedelse, per verificare i parametri e prendere decisioni in base alla temperatura raggiunta. - Il ciclo
for, per calcolare la temperatura a intervalli regolari. - Il ciclo
while, per proseguire il calcolo fino a quando il corpo raggiunge una determinata vicinanza alla temperatura ambiente. - Le funzioni, per separare le diverse operazioni matematiche e rendere il programma più leggibile e riutilizzabile.
Confronteremo inoltre i risultati ottenuti mediante il metodo numerico di Eulero con la soluzione analitica dell’equazione, per comprendere come la scelta del passo temporale influenzi la precisione della simulazione.
Il ruolo di GX-TXT
Come nelle altre lezioni del corso, GNU Octave sarà il linguaggio utilizzato per scrivere il programma, mentre GX-TXT fornirà l’ambiente di esecuzione e gli strumenti per gestire i risultati scientifici.
Non ci limiteremo quindi a stampare una tabella di numeri. Lo script produrrà due Dataset scientifici, uno contenente la soluzione numerica e l’altro quella analitica, che verranno registrati nella Session attraverso le API pubbliche del framework.
Infine, utilizzeremo le API di rappresentazione grafica di GX-TXT per confrontare visivamente le due curve.
L’esperimento è stato eseguito e verificato con GX-TXT 0.9.8.10-dev-111, patch P019, utilizzando i sorgenti pubblicati nel repository ufficiale GitHub (commit di riferimento).
Questo esempio ci permetterà quindi di apprendere contemporaneamente nuovi strumenti del linguaggio Octave e il corretto utilizzo delle API pubbliche di GX-TXT.
La legge di raffreddamento di Newton
Per descrivere matematicamente il raffreddamento del nostro corpo utilizziamo la legge di raffreddamento di Newton.
Secondo questo modello, la velocità con cui cambia la temperatura di un corpo è proporzionale alla differenza tra la sua temperatura e quella dell’ambiente circostante.
L’equazione differenziale che descrive il fenomeno è:
\[\frac{dT}{dt}=-k\left(T-T_{\mathrm{amb}}\right)\]
Dove:
- T è la temperatura del corpo, espressa in kelvin (K).
- Tamb è la temperatura dell’ambiente, anch’essa espressa in kelvin.
- t è il tempo.
- k è il coefficiente di raffreddamento, che dipende dalle caratteristiche del sistema e dalle condizioni di scambio termico.
- dT/dt rappresenta la velocità di variazione della temperatura rispetto al tempo.
Il segno negativo è fondamentale: quando il corpo è più caldo dell’ambiente, la differenza di temperatura è positiva e quindi la sua temperatura diminuisce nel tempo.
Quando invece la temperatura del corpo si avvicina a quella ambiente, la differenza diventa progressivamente più piccola e il raffreddamento rallenta.
Questo semplice modello presuppone che la temperatura del corpo sia uniforme, che l’ambiente mantenga una temperatura costante e che il coefficiente di raffreddamento non cambi durante l’esperimento.
Non pretende quindi di descrivere tutti i dettagli dello scambio termico, ma è particolarmente adatto a introdurre il calcolo numerico.
I parametri del nostro esperimento
Per rendere la simulazione riproducibile, assegniamo valori precisi alle grandezze fisiche.
| Parametro | Valore |
|---|---|
| Temperatura iniziale | 80 °C = 353,15 K |
| Temperatura ambiente | 22 °C = 295,15 K |
| Coefficiente di raffreddamento | 0,08 min−1 |
| Intervallo di calcolo | 0,5 minuti |
| Durata della simulazione | 60 minuti |
Poiché abbiamo scelto di esprimere il tempo in minuti, il coefficiente di raffreddamento deve avere unità di misura compatibili, ovvero min−1.
Per le temperature useremo i kelvin, l’unità SI della temperatura termodinamica, pur mostrando talvolta i risultati in gradi Celsius per facilitarne la lettura.
Dall’equazione differenziale al metodo di Eulero
L’equazione di Newton descrive una variazione continua della temperatura. Un programma numerico, invece, può calcolare il valore della temperatura in una successione di istanti separati.
Dobbiamo quindi trasformare il problema continuo in una procedura costituita da operazioni ripetibili.
Uno dei metodi più semplici per farlo è il metodo di Eulero esplicito.
L’idea consiste nell’utilizzare la velocità di variazione della temperatura all’istante corrente per stimare la temperatura all’istante successivo.
In generale, il metodo di Eulero si scrive:
\[y_{n+1}=y_n+\Delta t\,f(t_n,y_n)\]
Applicandolo alla nostra equazione di raffreddamento, otteniamo:
\[T_{n+1}=T_n-k\,\Delta t\left(T_n-T_{\mathrm{amb}}\right)\]
Questa formula è particolarmente interessante dal punto di vista della programmazione.
Per calcolare la temperatura successiva dobbiamo conoscere soltanto la temperatura corrente, quella ambiente, il coefficiente di raffreddamento e l’intervallo temporale.
Possiamo quindi ripetere esattamente la stessa operazione per tutti gli intervalli della simulazione.
Ed è proprio ciò che faremo utilizzando un ciclo for in GNU Octave.
Un esempio di calcolo manuale
All’inizio della simulazione abbiamo:
\[T_0=353.15\ \mathrm{K}\]
La temperatura ambiente vale:
\[T_{\mathrm{amb}}=295.15\ \mathrm{K}\]
Con un intervallo di 0,5 minuti e un coefficiente di raffreddamento pari a 0,08 min−1, la formula diventa:
\[T_1=353.15-0.08\cdot0.5\cdot(353.15-295.15)\]
Otteniamo:
\[T_1=350.83\ \mathrm{K}=77.68\,{}^\circ\mathrm{C}\]
Dopo appena 30 secondi, la temperatura è quindi diminuita di 2,32 °C.
Ora possiamo ripetere il calcolo utilizzando il risultato precedente:
\[T_2=350.83-0.08\cdot0.5\cdot(350.83-295.15)\]
Da cui:
\[T_2=348.6028\ \mathrm{K}=75.4528\,{}^\circ\mathrm{C}\]
Abbiamo così calcolato i primi due intervalli della simulazione.
Ripetendo questa operazione 120 volte, possiamo ricostruire l’andamento della temperatura durante tutti i 60 minuti previsti.
La temperatura iniziale viene conservata come primo campione: il risultato complessivo sarà quindi un vettore di 121 valori, associati ad altrettanti istanti temporali.
La soluzione analitica: il nostro riferimento
La legge di raffreddamento di Newton possiede anche una soluzione analitica, cioè una formula che permette di calcolare direttamente la temperatura in qualunque istante, senza ripetere tutti i passaggi precedenti.
La soluzione è:
\[T(t)=T_{\mathrm{amb}}+(T_0-T_{\mathrm{amb}})\,e^{-kt}\]
Dove e è la base dei logaritmi naturali e T0 rappresenta la temperatura iniziale.
Per il nostro esperimento, la soluzione diventa:
\[T(t)=295.15+58\,e^{-0.08t}\]
Con il tempo espresso in minuti.
Per esempio, dopo 60 minuti la soluzione analitica fornisce una temperatura di circa 22,4773 °C.
Il metodo di Eulero, con il passo temporale scelto, restituisce invece circa 22,4325 °C.
La differenza alla fine della simulazione è quindi di circa 0,0448 K.
Questa differenza è un errore di discretizzazione numerica: non deriva da misure sperimentali, ma dall’approssimazione introdotta dal metodo di Eulero.
L’errore massimo lungo l’intera simulazione può essere maggiore di quello osservato all’ultimo istante.
Confrontando le due soluzioni potremo verificare la qualità dell’approssimazione e comprendere perché la scelta del passo temporale sia importante.
Nel prossimo passaggio tradurremo queste formule in istruzioni GNU Octave, introducendo le condizioni, i cicli e le funzioni che costituiscono l’argomento centrale della lezione.
Dal modello matematico al programma Octave
Abbiamo visto come il metodo di Eulero permetta di calcolare una nuova temperatura partendo da quella precedente. Per realizzare una simulazione completa, tuttavia, non basta conoscere la formula matematica: dobbiamo costruire un programma capace di controllare i parametri, ripetere i calcoli e gestire eventuali situazioni anomale.
Cominciamo definendo i parametri del nostro esperimento in GNU Octave.
T_initial_K = 80 + 273.15;
T_ambient_K = 22 + 273.15;
cooling_k_per_min = 0.08;
dt_min = 0.5;
duration_min = 60;
threshold_K = 1.0;
max_threshold_steps = 120;
Ogni istruzione assegna un valore a una variabile. I nomi sono stati scelti per rendere immediatamente comprensibile il loro significato e, quando opportuno, l’unità di misura.
La variabile T_initial_K contiene la temperatura iniziale espressa in kelvin, mentre T_ambient_K contiene quella ambiente. Il coefficiente cooling_k_per_min definisce la velocità del raffreddamento, e dt_min stabilisce l’intervallo temporale utilizzato per ogni iterazione.
La variabile threshold_K rappresenta invece la massima differenza di temperatura che considereremo accettabile per dichiarare raggiunta la soglia prefissata.
Le condizioni: if, elseif ed else
In un programma scientifico è importante verificare che i parametri siano validi prima di iniziare qualsiasi elaborazione numerica.
Che cosa accadrebbe, per esempio, se impostassimo un intervallo temporale negativo? Oppure se scegliessimo un coefficiente di raffreddamento uguale a zero?
Per evitare risultati privi di significato o comportamenti imprevisti possiamo introdurre delle strutture condizionali.
In GNU Octave la forma generale di una struttura condizionale è:
if condition_1
% Instructions executed when condition_1 is true.
elseif condition_2
% Instructions executed when condition_2 is true.
else
% Instructions executed when no preceding condition is true.
endif
Il funzionamento è semplice: Octave valuta la prima condizione. Se risulta vera, esegue le istruzioni associate e salta gli altri rami della struttura.
Se la prima condizione è falsa, passa alla successiva introdotta da elseif. Il blocco else, quando presente, viene eseguito soltanto se nessuna delle condizioni precedenti risulta vera.
La parola endif conclude la struttura condizionale. In Octave è possibile utilizzare anche end, ma nel nostro corso preferiremo le terminazioni esplicite, perché rendono più riconoscibile la struttura del programma.
Un esempio pratico: classificare la temperatura
Nel nostro esperimento vogliamo distinguere tre situazioni:
- Il corpo si trova entro la tolleranza stabilita rispetto alla temperatura ambiente.
- Il corpo è vicino alla temperatura ambiente, ma non ha ancora raggiunto la tolleranza.
- Il corpo è ancora nella fase principale del raffreddamento.
Possiamo esprimere queste decisioni mediante la funzione classify_state(), presente nello script della lezione.
function label = classify_state(T, Ta, tolerance)
gap = abs(T - Ta);
if gap <= tolerance
label = 'Within ambient tolerance';
elseif gap <= 5 * tolerance
label = 'Approaching ambient temperature';
else
label = 'Cooling in progress';
endif
endfunction
Esaminiamo il funzionamento della funzione.
La prima istruzione calcola il valore assoluto della differenza tra la temperatura del corpo e quella ambiente mediante abs(). Utilizziamo il valore assoluto perché, ai fini della classificazione, ci interessa la distanza dalla temperatura ambiente, indipendentemente dal segno.
La prima condizione, gap <= tolerance, verifica se la differenza è minore o uguale alla tolleranza scelta. In questo caso la funzione restituisce il messaggio Within ambient tolerance.
Se la prima condizione è falsa, elseif verifica se la differenza è comunque inferiore o uguale a cinque volte la tolleranza. In tal caso, il corpo viene classificato come prossimo alla temperatura ambiente.
Se nessuna delle due condizioni è soddisfatta, viene eseguito il ramo else.
È importante sottolineare che queste categorie sono state definite per il nostro esperimento didattico: non rappresentano una classificazione fisica universale dello stato termico di un corpo.
Verificare i parametri prima del calcolo
Le strutture condizionali sono utili anche per controllare la validità degli input.
Nello script abbiamo definito una seconda funzione, chiamata validate_experiment(), che verifica i parametri prima dell’avvio della simulazione.
function validate_experiment(T0, Ta, k, dt, duration, tol, max_steps)
parameters = [T0, Ta, k, dt, duration, tol, max_steps];
if any(!isfinite(parameters))
error('All experiment parameters must be finite numbers.');
elseif T0 <= Ta
error('Initial temperature must exceed ambient temperature.');
elseif k <= 0 || dt <= 0 || duration <= 0 || tol <= 0
error('Rate, time step, duration, and tolerance must be positive.');
elseif k * dt > 1
error('Euler step is too large for monotonic cooling (k*dt > 1).');
elseif max_steps <= 0 || max_steps != round(max_steps)
error('Maximum iteration count must be a positive integer.');
endif
endfunction
In questa funzione incontriamo diverse caratteristiche importanti del linguaggio Octave:
isfinite()verifica che un valore numerico non sia infinito o NaN.any()restituisce vero quando almeno un elemento del vettore soddisfa la condizione indicata.!rappresenta la negazione logica.||rappresenta l’operatore logico OR con valutazione abbreviata, utilizzato qui per combinare condizioni scalari.error()interrompe l’esecuzione generando un messaggio diagnostico.
Il controllo k * dt > 1 ha inoltre un preciso significato numerico: per il nostro modello, con un corpo inizialmente più caldo dell’ambiente, imponiamo che il passo di Eulero non produca un superamento della temperatura ambiente. Non si tratta di una condizione generale di stabilità per tutti i metodi numerici.
Osserviamo infine che la funzione non contiene un blocco else. Questo è perfettamente valido: else e elseif sono facoltativi. Se tutti i controlli vengono superati, la funzione termina normalmente e il programma può proseguire.
Come vengono utilizzate le funzioni nello script
Nel programma completo, la verifica viene richiesta attraverso una semplice chiamata:
validate_experiment(T_initial_K, T_ambient_K, cooling_k_per_min, ...
dt_min, duration_min, threshold_K, max_threshold_steps);
Gli argomenti vengono passati alla funzione nello stesso ordine con cui sono dichiarati nella sua definizione.
La funzione non restituisce un valore numerico: il suo compito è verificare che tutti i parametri siano accettabili. Se trova un errore, interrompe il programma; altrimenti il controllo ritorna alla sequenza principale.
Nota sull’esecuzione in GX-TXT: nella versione collaudata del nostro script le funzioni vengono definite prima delle istruzioni che le richiamano. Il file contiene inoltre l’istruzione iniziale 1;, utilizzata per mantenere la corretta struttura di uno script Octave contenente definizioni di funzione. Questa disposizione è importante per il caricamento del sorgente attraverso il runner di GX-TXT.
Abbiamo così introdotto il primo elemento fondamentale del controllo di flusso: un programma può prendere decisioni differenti in funzione dei dati che riceve.
Nel prossimo passaggio vedremo come ripetere automaticamente il calcolo della temperatura attraverso il ciclo for, costruendo l’intera serie temporale del raffreddamento.
Approfondimento — Che cos’è una funzione in GNU Octave e perché la utilizziamo?
Che cos’è una funzione?
Quando programmiamo, capita spesso di dover eseguire la stessa operazione in punti diversi del programma.
Immaginiamo, per esempio, di dover convertire frequentemente delle temperature da gradi Celsius a kelvin.
Potremmo scrivere ogni volta:
T_kelvin = T_celsius + 273.15;
Ma se questa operazione dovesse essere ripetuta in molti punti del programma, sarebbe più conveniente definirla una sola volta e richiamarla quando necessario.
Una funzione è proprio questo: un blocco di istruzioni al quale assegniamo un nome, che può ricevere dei valori, elaborarli e restituire uno o più risultati.
Possiamo immaginare una funzione come una piccola macchina:
Valori in ingresso → Elaborazione → Risultati in uscita
Una volta definita, possiamo utilizzare questa macchina tutte le volte che vogliamo, senza riscriverne le istruzioni interne.
La nostra prima funzione
Definiamo una funzione che converta una temperatura da Celsius a kelvin:
function T_kelvin = celsius_to_kelvin(T_celsius)
T_kelvin = T_celsius + 273.15;
endfunction
Esaminiamo con attenzione la prima riga:
function T_kelvin = celsius_to_kelvin(T_celsius)
Questa istruzione contiene quattro elementi importanti.
functionindica che stiamo definendo una funzione.T_kelvinè il nome della variabile che conterrà il risultato da restituire.celsius_to_kelvinè il nome della funzione, che utilizzeremo per richiamarla.T_celsiusè il parametro di ingresso, cioè la variabile attraverso cui la funzione riceve il valore da elaborare.
La seconda riga contiene il calcolo vero e proprio:
T_kelvin = T_celsius + 273.15;
Infine, endfunction segnala la conclusione della definizione.
Come si richiama una funzione?
Definire una funzione non significa eseguire immediatamente il calcolo. Significa soltanto spiegare a Octave che cosa dovrà fare quando quella funzione verrà chiamata.
Per utilizzarla scriviamo:
risultato = celsius_to_kelvin(25);
Quando Octave incontra questa istruzione:
- Riconosce il nome della funzione
celsius_to_kelvin. - Passa il numero 25 al parametro di ingresso
T_celsius. - Esegue il calcolo
25 + 273.15. - Assegna il risultato 298,15 alla variabile di uscita
T_kelvin. - Restituisce questo valore al programma che ha chiamato la funzione.
La variabile risultato conterrà quindi il valore 298,15.
Possiamo richiamare la stessa funzione anche con valori differenti:
T1 = celsius_to_kelvin(0);
T2 = celsius_to_kelvin(20);
T3 = celsius_to_kelvin(100);
Otterremo rispettivamente 273,15 K, 293,15 K e 373,15 K.
Notiamo un dettaglio: non è necessario che la variabile utilizzata per ricevere il risultato abbia lo stesso nome della variabile di uscita definita nella funzione.
Possiamo scrivere T1, T2 oppure risultato. È il valore restituito che viene assegnato alla variabile scelta dal programma chiamante.
I parametri e gli argomenti sono la stessa cosa?
Nel linguaggio comune questi termini vengono spesso utilizzati come sinonimi, ma nella programmazione possiamo fare una distinzione utile.
I parametri sono i nomi dichiarati nella definizione della funzione. Gli argomenti sono invece i valori effettivamente forniti quando la richiamiamo.
Per esempio:
function result = double_value(number)
result = 2 * number;
endfunction
x = double_value(7);
In questo esempio number è il parametro della funzione, mentre 7 è l’argomento passato durante la chiamata.
La funzione restituisce 14, che viene assegnato a x.
Una funzione può ricevere più parametri
Nel nostro esperimento sulla legge di Newton utilizziamo la funzione cooling_step():
function next_T = cooling_step(T, Ta, k, dt)
next_T = T - k * dt * (T - Ta);
endfunction
Questa volta abbiamo quattro parametri di ingresso:
T: temperatura corrente del corpo.Ta: temperatura ambiente.k: coefficiente di raffreddamento.dt: intervallo temporale.
La funzione applica il metodo di Eulero e restituisce la temperatura corrispondente all’istante successivo.
Possiamo richiamarla direttamente:
next_temperature = cooling_step(353.15, 295.15, 0.08, 0.5);
Il risultato sarà 350,83 K.
È importante rispettare l’ordine degli argomenti: il primo valore viene associato a T, il secondo a Ta, il terzo a k e il quarto a dt.
Invertire due argomenti numerici può produrre risultati errati senza necessariamente provocare un errore di esecuzione.
Le variabili locali di una funzione
Un altro concetto importante è quello di ambito delle variabili, chiamato anche scope.
Le variabili e i parametri definiti all’interno di una normale funzione Octave appartengono al suo spazio di lavoro locale.
Consideriamo:
function result = square_number(x)
result = x * x;
endfunction
a = 5;
b = square_number(a);
La funzione riceve il valore di a attraverso il parametro locale x, esegue il calcolo e restituisce 25.
Nel programma principale, il risultato viene memorizzato in b.
La variabile locale x non diventa automaticamente una variabile disponibile nel programma principale.
Questa separazione è vantaggiosa perché evita che una funzione modifichi accidentalmente le variabili utilizzate da altre parti del programma.
Una funzione deve sempre restituire un valore?
No. Una funzione può anche svolgere un’operazione senza restituire un risultato esplicito.
Nel nostro script abbiamo, per esempio, validate_experiment().
Questa funzione controlla che i parametri dell’esperimento siano validi. Se trova un problema, genera un errore attraverso error(); altrimenti termina normalmente.
La sua definizione comincia infatti con:
function validate_experiment(T0, Ta, k, dt, duration, tol, max_steps)
Non compare una variabile di uscita, perché la funzione non deve restituire una temperatura o un numero: deve semplicemente eseguire una verifica.
Octave possiede già delle funzioni?
Certamente. GNU Octave mette a disposizione moltissime funzioni predefinite.
Ne abbiamo già incontrate alcune:
abs(x)calcola il valore assoluto.sqrt(x)calcola la radice quadrata.exp(x)calcola la funzione esponenziale.zeros(m,n)crea una matrice di zeri.round(x)arrotonda un numero all’intero più vicino.disp(x)visualizza un valore.
Quando utilizziamo queste funzioni non dobbiamo conoscerne l’implementazione interna: è sufficiente conoscere il loro scopo e il modo corretto di chiamarle.
Allo stesso modo possiamo creare le nostre funzioni per risolvere problemi specifici.
Funzioni e script: qual è la differenza?
Uno script è un file contenente istruzioni Octave eseguite nel contesto di lavoro corrente.
Una funzione possiede invece un proprio spazio di lavoro e può ricevere argomenti e restituire risultati in modo esplicito.
In GNU Octave possiamo definire funzioni in file dedicati oppure, rispettando le regole del linguaggio, all’interno di uno script.
Nel nostro caso GX-TXT esegue uno script registrato e ne gestisce automaticamente il collegamento con il framework.
Per assicurare la corretta disponibilità delle funzioni locali abbiamo utilizzato questa organizzazione:
1;
function result = double_value(number)
result = 2 * number;
endfunction
% Main script instructions.
value = double_value(7);
disp(value);
L’istruzione 1;, posta all’inizio, fa sì che il file venga trattato come script, anziché come file la cui prima istruzione effettiva è una definizione di funzione.
Le funzioni vengono definite prima delle istruzioni principali che le richiamano. Questa disposizione è stata adottata anche nello script della Lezione 04 dopo il primo collaudo con GX-TXT.
Perché è conveniente scrivere funzioni?
Utilizzare funzioni offre diversi vantaggi:
- Riutilizzo: una formula viene scritta una sola volta e può essere richiamata più volte.
- Leggibilità: un nome significativo come
cooling_step()permette di comprendere lo scopo di un’operazione senza esaminarne ogni dettaglio. - Manutenzione: se dobbiamo correggere il calcolo, possiamo intervenire in un unico punto.
- Verifica: possiamo controllare separatamente il comportamento di ciascuna funzione.
- Organizzazione: possiamo suddividere un problema complesso in operazioni più semplici.
Nel nostro esperimento utilizzeremo quattro funzioni con responsabilità differenti:
| Funzione | Scopo |
|---|---|
validate_experiment() | Controllare la validità dei parametri. |
cooling_step() | Calcolare un passaggio numerico con Eulero. |
cooling_exact() | Calcolare la temperatura mediante la soluzione analitica. |
classify_state() | Classificare lo stato del raffreddamento. |
In sintesi: una funzione permette di dare un nome a un’operazione, stabilire quali dati deve ricevere e quali risultati deve produrre. È uno degli strumenti fondamentali per costruire programmi scientifici ordinati, verificabili e riutilizzabili.
Ripetere un’operazione: il ciclo for
Immaginiamo di dover misurare la temperatura di un campione ogni 30 secondi, per un totale di 60 minuti.
Se volessimo annotare manualmente tutti i risultati, dovremmo ripetere la stessa operazione 120 volte. Analogamente, se dovessimo eseguire un calcolo per ogni intervallo, potremmo scrivere 120 istruzioni quasi identiche.
Naturalmente sarebbe un metodo poco pratico: il programma diventerebbe lungo, difficile da controllare e molto semplice da sbagliare.
La programmazione ci offre una soluzione: scrivere una sola volta le istruzioni e chiedere al computer di ripeterle automaticamente.
Questo è esattamente lo scopo di un ciclo.
In GNU Octave esistono diverse strutture per ripetere le istruzioni. Cominciamo con il ciclo for, particolarmente utile quando sappiamo quante volte dobbiamo eseguire un’operazione.
Il nostro primo ciclo for
Prima di affrontare il raffreddamento di Newton, proviamo un esempio molto semplice:
for n = 1:5
disp(n);
endfor
Questo programma è formato da appena tre righe, ma introduce un concetto fondamentale della programmazione.
Vediamo che cosa significa ogni istruzione.
Prima riga: for n = 1:5
La parola for indica a Octave che sta per iniziare un ciclo.
La variabile n viene utilizzata come contatore del ciclo. Durante l’esecuzione assumerà, uno dopo l’altro, i valori indicati dalla sequenza 1:5.
In GNU Octave, l’operatore due punti : permette di costruire una sequenza numerica. In questo caso genera i numeri interi da 1 a 5, estremi inclusi:
1 2 3 4 5
Seconda riga: disp(n);
La funzione disp() visualizza il valore corrente della variabile n.
Poiché questa istruzione si trova all’interno del ciclo, viene eseguita una volta per ciascun valore assunto da n.
Terza riga: endfor
Questa istruzione indica la fine del blocco che deve essere ripetuto.
Il risultato sarà la visualizzazione dei numeri da 1 a 5, uno per ogni iterazione.
Che cosa succede realmente durante l’esecuzione?
Per comprendere il ciclo, immaginiamo di osservare Octave mentre esegue il programma, un’istruzione alla volta.
| Iterazione | Valore di n | Operazione eseguita |
|---|---|---|
| 1 | 1 | Visualizza 1 |
| 2 | 2 | Visualizza 2 |
| 3 | 3 | Visualizza 3 |
| 4 | 4 | Visualizza 4 |
| 5 | 5 | Visualizza 5 |
Alla prima iterazione Octave assegna a n il valore 1 ed esegue disp(n). Terminata l’istruzione, il programma torna automaticamente all’inizio del ciclo e assegna a n il valore successivo, cioè 2.
La stessa operazione continua fino al valore 5. Quando tutti i valori della sequenza sono stati utilizzati, il ciclo termina e il programma passa all’istruzione successiva a endfor.
Ogni esecuzione del corpo del ciclo viene chiamata iterazione.
È importante comprendere che non stiamo copiando cinque volte la stessa istruzione: abbiamo scritto una sola istruzione e abbiamo indicato a Octave di eseguirla cinque volte, modificando automaticamente il valore del contatore.
L’operatore due punti e il passo della sequenza
Nel nostro primo esempio abbiamo utilizzato 1:5. Per impostazione predefinita, Octave incrementa i valori di un’unità.
Possiamo però specificare anche un incremento differente.
for n = 0:2:10
disp(n);
endfor
Questa volta il contatore assume i valori:
0
2
4
6
8
10
La sintassi completa è:
start_value : step_value : end_value
Il primo numero indica il valore iniziale, il secondo l’incremento e il terzo il limite finale della sequenza.
Possiamo anche utilizzare incrementi negativi, per esempio 5:-1:1, ottenendo la sequenza 5, 4, 3, 2, 1.
Il valore finale viene incluso soltanto quando è effettivamente raggiunto dalla sequenza. Per esempio, 0:2:5 produce 0, 2 e 4, ma non 5.
Il contatore non deve necessariamente chiamarsi n. Possiamo utilizzare nomi come i, step oppure index. Quest’ultimo sarà il nome impiegato nel nostro esperimento.
Utilizzare un ciclo for nella simulazione del raffreddamento
Torniamo ora al nostro problema scientifico.
Vogliamo calcolare la temperatura del corpo ogni 0,5 minuti, partendo da 80 °C e arrivando a un tempo complessivo di 60 minuti.
La prima domanda da porci è: quanti passaggi numerici dobbiamo eseguire?
Possiamo calcolarlo dividendo la durata complessiva per l’intervallo temporale: \[ N=\frac{60}{0.5}=120 \]
Il nostro programma dovrà quindi eseguire 120 aggiornamenti della temperatura.
Nello script il numero dei passaggi è calcolato mediante:
step_count = round(duration_min / dt_min);
La funzione round() arrotonda il risultato all’intero più vicino. In questo esperimento il rapporto è già un numero intero; il programma esegue inoltre un controllo per verificare che la durata sia effettivamente un multiplo del passo scelto.
Perché abbiamo 120 passaggi ma 121 temperature?
Questo è un dettaglio importante.
All’inizio della simulazione, cioè al tempo zero, conosciamo già la temperatura del corpo: 80 °C. Non dobbiamo calcolarla, perché è una delle condizioni iniziali del problema.
Successivamente calcoliamo una nuova temperatura ogni 30 secondi.
Avremo quindi:
| Posizione nel vettore | Tempo (min) | Temperatura |
|---|---|---|
| 1 | 0 | 80 °C, valore iniziale |
| 2 | 0,5 | Primo valore calcolato |
| 3 | 1,0 | Secondo valore calcolato |
| 4 | 1,5 | Terzo valore calcolato |
| … | … | … |
| 121 | 60,0 | Ultimo valore calcolato |
Il numero di temperature memorizzate è quindi dato da: \[ N_{\mathrm{campioni}}=N_{\mathrm{passaggi}}+1 \]
Nel nostro caso abbiamo 120 passaggi e 121 campioni.
Attenzione: in GNU Octave gli elementi di un vettore si numerano a partire da 1, non da 0. Il primo elemento è quindi T(1), il secondo è T(2) e così via.
Questa convenzione è particolarmente importante quando utilizziamo un ciclo per leggere o modificare gli elementi di un vettore.
Preparare i vettori prima di iniziare il ciclo
Nel nostro script utilizziamo queste istruzioni:
time_min = (0:step_count)' * dt_min;
T_euler_K = zeros(step_count + 1, 1);
T_exact_K = zeros(step_count + 1, 1);
absolute_error_K = zeros(step_count + 1, 1);
La prima istruzione costruisce il vettore dei tempi.
La sequenza 0:step_count genera i numeri da 0 a 120. L’apostrofo ' trasforma questa sequenza, inizialmente disposta su una riga, in un vettore colonna. Nel nostro caso, trattandosi di numeri reali, non vi è differenza pratica tra trasposizione e trasposizione coniugata.
Moltiplicando tutti gli elementi per dt_min, che vale 0,5, otteniamo i tempi effettivi: 0, 0,5, 1, 1,5 e così via fino a 60 minuti.
Le tre istruzioni successive utilizzano la funzione zeros().
Per esempio:
T_euler_K = zeros(121, 1);
crea un vettore colonna formato da 121 elementi, inizialmente tutti uguali a zero.
Perché creare un vettore di zeri se ancora non conosciamo le temperature?
Perché vogliamo riservare in anticipo lo spazio necessario a memorizzare i risultati. Questa tecnica si chiama preallocazione.
È generalmente più efficiente rispetto ad allungare ripetutamente un vettore durante ogni iterazione, soprattutto quando dobbiamo elaborare migliaia o milioni di valori.
Abbiamo preparato tre vettori:
T_euler_K, destinato alle temperature calcolate numericamente;T_exact_K, destinato alle temperature della soluzione analitica;absolute_error_K, destinato alle differenze assolute tra i due risultati.
Il vettore time_min contiene invece i tempi corrispondenti a ciascun campione.
Inserire la temperatura iniziale
Prima di iniziare il ciclo assegniamo le condizioni iniziali:
T_euler_K(1) = T_initial_K;
T_exact_K(1) = T_initial_K;
Le parentesi tonde indicano la posizione dell’elemento del vettore che vogliamo modificare.
La prima istruzione significa: inserisci il valore della temperatura iniziale nella prima posizione del vettore T_euler_K.
La seconda esegue la stessa operazione per il vettore della soluzione analitica.
Poiché i due metodi descrivono lo stesso esperimento, devono necessariamente partire dalla medesima temperatura iniziale.
Il ciclo for completo del nostro esperimento
Adesso possiamo esaminare il ciclo realmente utilizzato nello script della Lezione 04.
for index = 2:(step_count + 1)
T_euler_K(index) = cooling_step(T_euler_K(index - 1), ...
T_ambient_K, cooling_k_per_min, dt_min);
T_exact_K(index) = cooling_exact(time_min(index), T_initial_K, ...
T_ambient_K, cooling_k_per_min);
endfor
Questo blocco è il cuore della simulazione.
Le istruzioni contenute al suo interno saranno eseguite 120 volte, una per ciascun nuovo intervallo temporale.
Prima riga: perché il ciclo comincia da 2?
La dichiarazione:
for index = 2:(step_count + 1)
indica che la variabile index deve assumere tutti i valori interi da 2 a 121.
Non iniziamo da 1 perché la prima posizione contiene già la temperatura iniziale. Dobbiamo calcolare soltanto i valori successivi.
Il primo valore di index sarà quindi 2, il successivo 3 e così via fino a 121.
Notiamo anche un’altra distinzione fondamentale: index non rappresenta il tempo fisico, ma la posizione del campione nel vettore.
Quando index vale 2, il tempo corrispondente è 0,5 minuti, non 2 minuti. Il tempo effettivo si trova in time_min(index).
Seconda riga: calcolare la nuova temperatura
Esaminiamo l’istruzione principale:
T_euler_K(index) = cooling_step(T_euler_K(index - 1), ...
T_ambient_K, cooling_k_per_min, dt_min);
Per comprenderla, dobbiamo leggere separatamente la parte a destra e quella a sinistra del segno uguale.
La parte destra richiama la funzione cooling_step(), fornendole quattro valori:
- la temperatura calcolata al passaggio precedente;
- la temperatura ambiente;
- il coefficiente di raffreddamento;
- l’intervallo temporale.
La funzione calcola la nuova temperatura applicando il metodo di Eulero e restituisce il risultato.
La parte sinistra indica dove memorizzare questo risultato: nella posizione index del vettore T_euler_K.
L’espressione index - 1 è particolarmente importante.
Indica che per calcolare una nuova temperatura dobbiamo recuperare quella memorizzata nella posizione precedente.
Per esempio, quando index vale 2, l’espressione index - 1 vale 1. Octave legge quindi T_euler_K(1), che contiene la temperatura iniziale.
Quando index vale 3, legge invece T_euler_K(2), cioè il risultato calcolato durante la prima iterazione.
È questo collegamento tra il risultato precedente e quello successivo che permette di costruire progressivamente l’intera simulazione.
Seguiamo manualmente le prime due iterazioni
Prima di iniziare il ciclo, la situazione è la seguente:
T_euler_K(1) = 353.15 K
index = 2
Prima iterazione
Il programma calcola: \[ T_1=353.15-0.08\cdot0.5\cdot(353.15-295.15) \]
Il risultato è 350,83 K, che viene memorizzato nella seconda posizione:
T_euler_K(2) = 350.83;
A questo punto il vettore contiene due temperature significative:
Position 1: 353.15 K
Position 2: 350.83 K
Seconda iterazione
Il contatore passa automaticamente al valore 3.
Questa volta il programma utilizza la temperatura appena calcolata, cioè 350,83 K, per ottenere il valore successivo: \[ T_2=350.83-0.08\cdot0.5\cdot(350.83-295.15) \]
Il risultato, 348,6028 K, viene memorizzato nella terza posizione:
T_euler_K(3) = 348.6028;
Il vettore contiene ora:
Position 1: 353.1500 K
Position 2: 350.8300 K
Position 3: 348.6028 K
Alla terza iterazione il programma leggerà il terzo elemento per calcolare il quarto, e così via.
Non è necessario scrivere altre formule: il ciclo si occupa di ripetere automaticamente tutte le operazioni.
Terza riga: calcolare anche la soluzione analitica
All’interno dello stesso ciclo compare una seconda istruzione:
T_exact_K(index) = cooling_exact(time_min(index), T_initial_K, ...
T_ambient_K, cooling_k_per_min);
Questa istruzione richiama la funzione cooling_exact(), che utilizza la formula analitica della legge di Newton.
A differenza del metodo di Eulero, la soluzione analitica non ha bisogno della temperatura calcolata al passaggio precedente. Le sono sufficienti il tempo corrente, la temperatura iniziale, la temperatura ambiente e il coefficiente di raffreddamento.
Per ottenere il tempo utilizziamo time_min(index).
Alla prima iterazione questo valore è 0,5 minuti; alla seconda è 1 minuto; all’ultima è 60 minuti.
In questo modo, durante ogni iterazione calcoliamo due temperature corrispondenti allo stesso istante: una mediante Eulero e una mediante la soluzione analitica.
Questo ci permetterà successivamente di confrontare i due metodi senza dover interpolare i risultati.
Perché utilizziamo tre puntini alla fine delle righe?
Avrai notato che alcune istruzioni terminano con tre puntini:
T_euler_K(index) = cooling_step(T_euler_K(index - 1), ...
T_ambient_K, cooling_k_per_min, dt_min);
In GNU Octave la sequenza ... permette di continuare un’istruzione sulla riga successiva.
Serve principalmente a migliorare la leggibilità del programma. Senza questa possibilità, le chiamate a funzioni con molti parametri potrebbero diventare eccessivamente lunghe.
I tre puntini non effettuano alcun calcolo e non modificano il risultato della simulazione.
Un secondo ciclo for: calcolare l’errore numerico
Dopo aver calcolato le temperature, il nostro script utilizza un secondo ciclo per determinare la differenza tra soluzione numerica e soluzione analitica.
for index = 1:(step_count + 1)
if index == 1
continue;
endif
absolute_error_K(index) = abs(T_euler_K(index) - T_exact_K(index));
endfor
Questa volta il ciclo comincia da 1, perché vogliamo esaminare tutti i campioni, compreso quello iniziale.
L’istruzione:
absolute_error_K(index) = abs(T_euler_K(index) - T_exact_K(index));
calcola la differenza assoluta tra le temperature ottenute dai due metodi e la memorizza nel vettore absolute_error_K.
Utilizziamo abs() perché ci interessa la grandezza dell’errore, indipendentemente dal suo segno.
Ma all’inizio del ciclo incontriamo anche una nuova istruzione:
if index == 1
continue;
endif
La parola continue: saltare un’iterazione
L’istruzione continue ordina a Octave di interrompere l’iterazione corrente e passare direttamente alla successiva.
Nel nostro esempio, quando index vale 1, il programma esegue continue e non calcola nuovamente l’errore del primo campione.
Perché possiamo farlo?
Perché il primo campione è identico nei due metodi: entrambi partono dalla stessa temperatura iniziale. La differenza è quindi esattamente zero.
Inoltre, il vettore absolute_error_K era già stato inizializzato con tutti gli elementi uguali a zero.
Quando index passa a 2, la condizione index == 1 diventa falsa e il programma esegue normalmente il calcolo dell’errore.
Notiamo che == è un operatore di confronto: verifica se due valori sono uguali. Non deve essere confuso con il singolo =, che utilizziamo per effettuare un’assegnazione.
È importante distinguere anche continue da break: continue salta soltanto l’iterazione corrente, mentre break termina completamente il ciclo. Utilizzeremo quest’ultima istruzione quando studieremo il ciclo while.
Qual è il risultato finale dei due cicli?
Al termine del primo ciclo avremo due vettori di 121 temperature:
- la serie numerica ottenuta con il metodo di Eulero;
- la serie analitica calcolata agli stessi istanti temporali.
Il secondo ciclo avrà invece popolato il vettore delle differenze assolute tra le due soluzioni.
Possiamo quindi utilizzare una semplice istruzione per trovare l’errore più grande:
max(absolute_error_K)
La funzione max() restituisce il massimo valore presente nel vettore.
Nel nostro esperimento il massimo errore assoluto è risultato pari a circa 0,434 K. Questo valore è diverso dall’errore all’ultimo istante, perché la differenza tra le due soluzioni cambia durante la simulazione.
Che cosa abbiamo imparato con il ciclo for?
Il ciclo for ci ha permesso di trasformare una singola formula matematica in una simulazione completa, senza riscrivere manualmente centinaia di istruzioni.
Abbiamo inoltre introdotto concetti che ritroveremo continuamente nella programmazione scientifica:
- Il contatore identifica la posizione corrente nella sequenza di iterazioni.
- L’indicizzazione permette di leggere e modificare singoli elementi di vettori e matrici.
- La preallocazione prepara lo spazio necessario per memorizzare i risultati.
- Le funzioni permettono di riutilizzare una stessa operazione matematica.
- L’istruzione continue consente di saltare un’iterazione quando una condizione lo richiede.
Un ultimo aspetto merita attenzione: il ciclo for è particolarmente adatto quando conosciamo in anticipo la sequenza delle iterazioni.
Ma cosa succede se non sappiamo quanti passaggi saranno necessari?
Nel nostro caso, per esempio, potremmo voler interrompere la simulazione non dopo 60 minuti, ma quando la temperatura del corpo si trova a meno di 1 °C da quella ambiente.
Non conosciamo necessariamente in anticipo il numero di iterazioni richieste: dipenderà dai parametri del modello.
Per affrontare questo problema utilizzeremo un’altra struttura fondamentale di GNU Octave: il ciclo while.
Il ciclo while: ripetere un calcolo finché una condizione è vera
Abbiamo utilizzato il ciclo for per calcolare la temperatura del corpo durante 60 minuti, con intervalli regolari di 30 secondi.
In quel caso conoscevamo già il numero delle iterazioni: 120.
Immaginiamo ora di voler rispondere a una domanda leggermente diversa:
Quanto tempo deve trascorrere affinché la temperatura del corpo si avvicini a meno di 1 °C dalla temperatura ambiente?
In questo caso non vogliamo necessariamente simulare un intervallo temporale prestabilito. Vogliamo invece continuare il calcolo finché non viene raggiunta una determinata condizione.
Per affrontare questo problema GNU Octave mette a disposizione il ciclo while.
La differenza tra for e while
Un ciclo for percorre una sequenza di valori già definita. Un ciclo while, invece, continua a eseguire le istruzioni finché una condizione rimane vera.
La struttura generale è:
while condition
% Instructions repeated while the condition is true.
endwhile
La parola inglese while significa “mentre”. Possiamo quindi leggere questa struttura come: mentre la condizione è vera, continua a eseguire le istruzioni.
Una caratteristica importante è che la condizione viene verificata prima di ogni iterazione.
Se risulta falsa già al primo controllo, le istruzioni all’interno del ciclo non vengono eseguite nemmeno una volta.
Un primo esempio senza formule fisiche
Consideriamo questo semplice programma:
n = 1;
while n <= 5
disp(n);
n = n + 1;
endwhile
Prima del ciclo inizializziamo la variabile n al valore 1.
La condizione n <= 5 significa che il ciclo deve continuare finché n è minore o uguale a 5.
All’interno del ciclo eseguiamo due operazioni: visualizziamo il valore corrente di n e poi lo incrementiamo di un’unità.
Attenzione all’istruzione:
n = n + 1;
Non rappresenta un’equazione matematica da risolvere. È un’assegnazione: Octave legge il valore attuale di n, gli aggiunge 1 e memorizza il risultato nuovamente nella stessa variabile.
Se n vale 3, dopo questa istruzione varrà 4.
Seguiamo l’esecuzione:
| Controllo | Valore di n | n <= 5? | Azione |
|---|---|---|---|
| 1 | 1 | Vero | Stampa 1 e incrementa |
| 2 | 2 | Vero | Stampa 2 e incrementa |
| 3 | 3 | Vero | Stampa 3 e incrementa |
| 4 | 4 | Vero | Stampa 4 e incrementa |
| 5 | 5 | Vero | Stampa 5 e incrementa |
| 6 | 6 | Falso | Termina il ciclo |
Il risultato è identico a quello del nostro primo ciclo for: vengono visualizzati i numeri da 1 a 5.
La differenza consiste nel modo in cui controlliamo l’esecuzione. Nel ciclo for il contatore assume automaticamente i valori di una sequenza; nel ciclo while dobbiamo modificare esplicitamente le variabili che determinano la condizione di uscita.
Un errore frequente: dimenticare di aggiornare la condizione
Consideriamo cosa accadrebbe eliminando l’istruzione n = n + 1:
n = 1;
while n <= 5
disp(n);
endwhile
La variabile n rimarrebbe sempre uguale a 1. Di conseguenza, la condizione n <= 5 sarebbe sempre vera.
Il programma continuerebbe a visualizzare il numero 1 senza terminare spontaneamente.
Questo comportamento viene chiamato ciclo infinito.
Un ciclo infinito può bloccare un’elaborazione, consumare inutilmente risorse e rendere necessario interrompere manualmente il programma.
Per questo motivo è buona pratica verificare sempre che un ciclo while possieda una condizione di terminazione raggiungibile e, quando si svolgono simulazioni numeriche, anche un limite massimo di iterazioni.
Applicare while al raffreddamento di Newton
Torniamo al nostro esperimento.
Il corpo parte da 80 °C, mentre l’ambiente si trova a 22 °C. Vogliamo interrompere la simulazione quando la differenza tra le due temperature diventa minore o uguale a 1 °C.
Poiché stiamo confrontando differenze di temperatura, una differenza di 1 °C corrisponde esattamente a una differenza di 1 K.
La nostra condizione di arresto è quindi: \[ |T-T_{\mathrm{amb}}|\leq 1\ \mathrm{K} \]
Nel linguaggio Octave possiamo scriverla così:
abs(T_threshold_K - T_ambient_K) <= threshold_K
La funzione abs() calcola il valore assoluto della differenza, mentre threshold_K contiene la tolleranza scelta, pari a 1 K.
Questa espressione diventa vera soltanto quando abbiamo raggiunto la vicinanza desiderata alla temperatura ambiente.
Preparare le variabili
Lo script inizializza il calcolo nel modo seguente:
T_threshold_K = T_initial_K;
threshold_steps = 0;
La prima istruzione assegna alla variabile T_threshold_K la temperatura iniziale del corpo.
La seconda crea un contatore che registra quanti aggiornamenti numerici abbiamo effettuato.
Notiamo che in questa parte dell’esperimento non stiamo preparando un vettore per conservare tutte le temperature. Vogliamo soltanto determinare quando viene raggiunta la soglia, quindi è sufficiente mantenere in memoria la temperatura corrente e il numero dei passaggi eseguiti.
Il ciclo while utilizzato nello script
Esaminiamo ora il codice effettivamente utilizzato nella Lezione 04:
while threshold_steps < max_threshold_steps
if abs(T_threshold_K - T_ambient_K) <= threshold_K
break;
endif
T_threshold_K = cooling_step(T_threshold_K, T_ambient_K, ...
cooling_k_per_min, dt_min);
threshold_steps = threshold_steps + 1;
endwhile
Questo ciclo introduce un elemento particolarmente importante: due meccanismi differenti che possono determinarne la terminazione.
Il primo è il limite massimo delle iterazioni. Il secondo è il raggiungimento della temperatura desiderata.
Primo controllo: il numero massimo di iterazioni
La prima riga è:
while threshold_steps < max_threshold_steps
La variabile max_threshold_steps vale 120.
Il ciclo può quindi continuare soltanto se il numero di aggiornamenti già effettuati è inferiore a 120.
Questo controllo costituisce una misura di sicurezza. Anche se la temperatura non raggiungesse la soglia stabilita, il programma non potrebbe continuare a effettuare aggiornamenti indefinitamente.
Il limite non garantisce che il nostro obiettivo fisico venga raggiunto: garantisce soltanto che il calcolo termini entro un numero definito di passaggi.
Secondo controllo: abbiamo raggiunto la temperatura desiderata?
All’interno del ciclo troviamo:
if abs(T_threshold_K - T_ambient_K) <= threshold_K
break;
endif
Questa struttura condizionale verifica se il corpo è sufficientemente vicino alla temperatura ambiente.
Se la condizione è vera, viene eseguita l’istruzione break.
break interrompe immediatamente il ciclo che la contiene.
Non viene eseguito il resto dell’iterazione e il programma continua dalla prima istruzione successiva a endwhile.
Ricordiamo la distinzione introdotta nella sezione precedente: continue salta soltanto l’iterazione corrente; break termina completamente il ciclo.
Nel nostro caso utilizziamo break perché, una volta raggiunta la soglia, non abbiamo alcun motivo per continuare il raffreddamento numerico.
Terza operazione: aggiornare la temperatura
Se la soglia non è stata raggiunta, il programma esegue:
T_threshold_K = cooling_step(T_threshold_K, T_ambient_K, ...
cooling_k_per_min, dt_min);
Riconosciamo la funzione cooling_step(), già utilizzata nel ciclo for.
La funzione riceve la temperatura corrente e restituisce quella calcolata per l’intervallo successivo.
Il risultato viene assegnato alla stessa variabile T_threshold_K, sostituendone il valore precedente.
Questo è un vantaggio delle funzioni: possiamo riutilizzare una stessa operazione matematica in punti diversi del programma, senza dover riscrivere ogni volta la formula di Eulero.
Quarta operazione: aggiornare il contatore
Infine troviamo:
threshold_steps = threshold_steps + 1;
Questa istruzione incrementa il numero di passaggi effettuati.
Il contatore è indispensabile per due ragioni.
Innanzitutto permette di rispettare il limite massimo delle iterazioni. Inoltre ci consente di calcolare il tempo trascorso moltiplicando il numero dei passaggi per la durata di ciascun intervallo.
È importante notare l’ordine delle istruzioni: prima aggiorniamo la temperatura e poi incrementiamo il contatore. Ogni incremento corrisponde quindi a un aggiornamento numerico realmente eseguito.
Seguire l’evoluzione del ciclo
All’inizio della simulazione la differenza tra il corpo e l’ambiente è di 58 K.
A ogni passaggio, con i parametri scelti, questa differenza viene moltiplicata per 0,96.
La differenza dopo un numero intero di passaggi è quindi: \[ \Delta T_n=58(0.96)^n\ \mathrm{K} \]
Possiamo seguire alcuni momenti significativi del calcolo:
| Passaggi eseguiti | Tempo (min) | Differenza dall’ambiente (K) |
|---|---|---|
| 0 | 0 | 58,0000 |
| 1 | 0,5 | 55,6800 |
| 2 | 1,0 | 53,4528 |
| 99 | 49,5 | Circa 1,019 |
| 100 | 50,0 | Circa 0,978 |
Dopo 99 passaggi la differenza è ancora superiore alla tolleranza di 1 K.
Il programma esegue quindi un altro aggiornamento. Al passaggio successivo la differenza scende sotto 1 K.
Quando la condizione viene controllata nuovamente, l’istruzione break interrompe il ciclo.
Il risultato del nostro esperimento è quindi:
La soglia viene raggiunta dopo 100 passaggi, corrispondenti a 50 minuti di tempo simulato.
Il valore non coincide necessariamente con l’istante esatto in cui la soluzione continua attraversa la soglia: dipende anche dall’intervallo temporale di 0,5 minuti scelto per la simulazione.
Verificare che il ciclo sia terminato correttamente
Dopo endwhile, lo script esegue un’ulteriore verifica:
if abs(T_threshold_K - T_ambient_K) > threshold_K
error('Temperature threshold was not reached within the step limit.');
endif
Perché questo controllo è necessario?
Perché il ciclo potrebbe essere terminato per due motivi diversi: il raggiungimento della soglia oppure il raggiungimento del numero massimo di passaggi.
Il semplice fatto che il ciclo sia terminato non significa quindi che la simulazione abbia raggiunto l’obiettivo.
Se la differenza di temperatura è ancora superiore alla tolleranza, il programma genera un errore esplicito.
In questo modo evitiamo di interpretare come riuscito un calcolo che, in realtà, non ha raggiunto la condizione richiesta.
Questo principio è fondamentale nella programmazione scientifica: un programma che termina senza bloccarsi non ha necessariamente prodotto un risultato valido.
Calcolare il tempo trascorso
Superata la verifica, possiamo calcolare il tempo necessario per raggiungere la soglia:
threshold_time_min = threshold_steps * dt_min;
Nel nostro esperimento abbiamo:
threshold_steps = 100
dt_min = 0.5
threshold_time_min = 50
Il tempo ottenuto è quindi 50 minuti.
Osserviamo che il ciclo while non ha bisogno di conoscere anticipatamente questo valore. Lo determina eseguendo la simulazione e controllando progressivamente la temperatura.
Quando scegliere for e quando scegliere while?
Abbiamo utilizzato due strutture differenti per affrontare due varianti dello stesso problema.
| Caratteristica | for | while |
|---|---|---|
| Controllo delle iterazioni | Sequenza predefinita | Condizione logica |
| Numero di passaggi | Determinato dalla sequenza | Dipende dall’evoluzione del calcolo |
| Gestione del contatore | Automatica | Esplicita, quando necessaria |
| Applicazione nell’esperimento | Calcolare 60 minuti di temperature | Attendere il raggiungimento della soglia |
| Precauzione principale | Controllare indici e dimensioni | Garantire condizioni di terminazione |
Non esiste una regola che imponga sempre l’uso di una struttura al posto dell’altra. Un problema può essere risolto in modi differenti e, in alcuni casi, entrambe le strutture possono essere appropriate.
La scelta dipende soprattutto da quale formulazione rende il programma più semplice da comprendere e verificare.
Nel nostro caso il ciclo for descrive naturalmente una simulazione a durata prefissata, mentre il ciclo while esprime direttamente la necessità di proseguire fino al raggiungimento di una condizione.
Abbiamo così completato lo studio delle principali strutture di controllo utilizzate nel nostro esperimento.
Nel prossimo passaggio approfondiremo un altro elemento fondamentale della programmazione GNU Octave: le funzioni. Impareremo a definirle, a passare loro dei parametri, a recuperare i risultati e a organizzare correttamente il codice all’interno degli script.
Organizzare un programma scientifico attraverso le funzioni
Abbiamo imparato a utilizzare le condizioni if, i cicli for e while, e abbiamo visto come una funzione permetta di racchiudere un’operazione all’interno di un blocco di codice riutilizzabile.
Ora possiamo esaminare come questi elementi collaborano nello script della nostra simulazione.
Per organizzare il programma abbiamo creato quattro funzioni, ciascuna con uno scopo preciso:
| Funzione | Responsabilità | Risultato |
|---|---|---|
validate_experiment() | Verificare i parametri | Termina normalmente oppure genera un errore |
cooling_step() | Applicare un passo del metodo di Eulero | Temperatura successiva |
cooling_exact() | Calcolare la soluzione analitica | Temperatura all’istante richiesto |
classify_state() | Classificare lo stato del raffreddamento | Messaggio descrittivo |
Questa suddivisione non è obbligatoria: potremmo scrivere tutte le operazioni direttamente nello script principale. Tuttavia, separare le responsabilità rende il programma più semplice da leggere, controllare e modificare.
La funzione validate_experiment(): controllare prima di calcolare
Abbiamo già incontrato questa funzione parlando delle condizioni. Vediamone ora il ruolo nell’organizzazione complessiva del programma.
La sua dichiarazione è:
function validate_experiment(T0, Ta, k, dt, duration, tol, max_steps)
La funzione riceve sette parametri. Alcuni descrivono il sistema fisico, mentre altri controllano il comportamento della simulazione numerica.
La funzione verifica che tutti i valori siano finiti, che le grandezze che devono essere positive lo siano effettivamente, che la temperatura iniziale superi quella ambiente e che il passo temporale soddisfi i vincoli adottati per il metodo di Eulero.
In particolare, il prodotto k * dt deve essere minore o uguale a 1 per garantire che, nel nostro modello, ogni passo di Eulero mantenga la temperatura tra quella corrente e quella ambiente.
Se un controllo fallisce, viene eseguita un’istruzione error(), che interrompe il programma segnalando il problema.
Se tutti i controlli vengono superati, la funzione termina normalmente senza restituire un valore.
Questo è un esempio di funzione utilizzata per validare le condizioni di esecuzione anziché eseguire direttamente un calcolo scientifico.
La funzione cooling_step(): un piccolo calcolo ripetuto molte volte
Questa è la funzione centrale del nostro modello numerico.
function next_T = cooling_step(T, Ta, k, dt)
next_T = T - k * dt * (T - Ta);
endfunction
La funzione riceve quattro parametri e restituisce una sola temperatura, indicata dalla variabile next_T.
La formula interna è esattamente quella ottenuta applicando il metodo di Eulero alla legge di raffreddamento di Newton.
Facciamo una prova con i valori iniziali:
T_new = cooling_step(353.15, 295.15, 0.08, 0.5);
disp(T_new);
Il risultato è 350,83 K.
Un aspetto interessante è che la funzione non deve sapere se viene richiamata dal ciclo for, dal ciclo while oppure da una semplice istruzione.
Riceve i valori, esegue il calcolo e restituisce il risultato.
Questo comportamento rende la funzione indipendente dal punto del programma in cui viene utilizzata.
Se in futuro decidessimo di modificare il metodo numerico, per esempio introducendo un algoritmo più accurato, potremmo realizzare una nuova funzione e modificare le chiamate interessate senza riscrivere completamente i cicli che gestiscono la simulazione.
La funzione cooling_exact(): calcolare una temperatura senza iterazioni
Il metodo di Eulero ricostruisce l’evoluzione della temperatura attraverso una successione di piccoli passaggi. La soluzione analitica permette invece di calcolare direttamente la temperatura in un determinato istante.
Nello script abbiamo definito:
function T = cooling_exact(time_min, T0, Ta, k)
T = Ta + (T0 - Ta) * exp(-k * time_min);
endfunction
La funzione contiene una singola istruzione matematica, ma utilizza un’importante funzione predefinita di Octave: exp().
La funzione exp(x) calcola l’esponenziale naturale di x, cioè il numero di Eulero elevato alla potenza indicata.
Per esempio:
T_at_10min = cooling_exact(10, 353.15, 295.15, 0.08);
Questa istruzione calcola direttamente la temperatura del corpo dopo 10 minuti.
Notiamo che non è necessario calcolare prima le temperature a 0,5, 1,0, 1,5 minuti e così via.
La formula analitica utilizza direttamente il tempo richiesto.
Nel ciclo for richiamiamo questa funzione per ogni elemento del vettore dei tempi, ottenendo così una serie di riferimento da confrontare con quella calcolata mediante Eulero.
La funzione classify_state(): restituire una risposta testuale
Finora abbiamo incontrato funzioni che eseguono controlli o restituiscono numeri.
Una funzione può però restituire anche una stringa di testo.
È quello che avviene con classify_state():
function label = classify_state(T, Ta, tolerance)
gap = abs(T - Ta);
if gap <= tolerance
label = 'Within ambient tolerance';
elseif gap <= 5 * tolerance
label = 'Approaching ambient temperature';
else
label = 'Cooling in progress';
endif
endfunction
La funzione riceve la temperatura del corpo, quella ambiente e una tolleranza.
Calcola la differenza assoluta tra le due temperature e sceglie uno dei tre messaggi disponibili.
Il valore restituito non rappresenta una grandezza fisica, ma una descrizione dello stato della simulazione.
Nel nostro script la funzione viene richiamata mediante:
final_state = classify_state(T_euler_K(end), T_ambient_K, threshold_K);
Qui incontriamo una caratteristica utile di Octave: end, utilizzato all’interno dell’indicizzazione di un vettore, indica la posizione dell’ultimo elemento.
Quindi T_euler_K(end) significa: recupera l’ultima temperatura calcolata dal metodo di Eulero.
Non dobbiamo confondere questo utilizzo di end con quello che può terminare una struttura di controllo. In questo caso la parola si trova tra parentesi tonde e serve a individuare un elemento del vettore.
Poiché la temperatura finale è sufficientemente vicina a quella ambiente, la funzione restituisce:
Within ambient tolerance
Come collaborano le funzioni durante l’esecuzione?
Ora che conosciamo lo scopo delle quattro funzioni, possiamo osservare il programma nel suo insieme.
L’esecuzione segue una sequenza logica:
- Lo script assegna i valori iniziali ai parametri dell’esperimento.
validate_experiment()controlla che i parametri siano validi.- Il ciclo
forrichiama ripetutamentecooling_step()ecooling_exact()per costruire le due serie temporali. - Un secondo ciclo calcola le differenze assolute tra le due soluzioni.
- Il ciclo
whileutilizza nuovamentecooling_step()per determinare quando viene raggiunta la soglia stabilita. classify_state()produce una descrizione dello stato finale.- Lo script presenta i risultati numerici e passa alle operazioni di pubblicazione attraverso le API pubbliche di GX-TXT.
Notiamo che cooling_step() viene utilizzata da due cicli differenti.
È proprio questo uno dei principali vantaggi delle funzioni: la stessa operazione può essere riutilizzata in contesti diversi, mantenendo un’unica definizione del calcolo.
Un programma organizzato è anche più facile da verificare
Separare le operazioni in funzioni permette di controllare ogni parte del programma in modo indipendente.
Per esempio, prima di eseguire l’intera simulazione possiamo verificare che cooling_step() restituisca 350,83 K quando riceve i parametri del primo intervallo.
Possiamo controllare che cooling_exact() restituisca esattamente la temperatura iniziale quando il tempo è zero.
Possiamo anche verificare che classify_state() restituisca il messaggio corretto per differenti valori della temperatura.
Questo modo di procedere costituisce una prima introduzione al concetto di test delle funzioni: invece di verificare soltanto il risultato finale del programma, controlliamo anche il comportamento delle sue singole componenti.
È una pratica particolarmente importante nel software scientifico, dove un programma può essere eseguito senza errori informatici e produrre comunque risultati numericamente o fisicamente sbagliati.
Dalla programmazione Octave all’integrazione con GX-TXT
Fino a questo punto abbiamo utilizzato esclusivamente il linguaggio GNU Octave e le sue funzioni matematiche.
Abbiamo calcolato le temperature, costruito vettori e verificato i risultati. Tuttavia, questi dati esistono ancora come variabili numeriche nello spazio di esecuzione dello script.
Per integrarli realmente nel nostro ambiente scientifico dobbiamo compiere un ulteriore passaggio: trasformare i risultati in Dataset canonici GX-TXT, dotati di identificatori, unità di misura e informazioni sulla loro origine.
Per farlo non accederemo direttamente alle directory della Session né modificheremo i file interni del framework.
Utilizzeremo invece lo Script Bridge e le API pubbliche GX-TXT, che permettono a GNU Octave di richiedere servizi scientifici al framework attraverso interfacce documentate.
La prossima sezione sarà dedicata proprio a queste API: vedremo come scoprirne la disponibilità, come richiamarle e come pubblicare i risultati del nostro esperimento in modo riproducibile.

GNU Octave e GX-TXT: lavorare attraverso le API pubbliche
Fino a questo punto abbiamo utilizzato GNU Octave per eseguire i calcoli necessari alla simulazione del raffreddamento di Newton.
Il programma ha costruito i vettori temporali, calcolato le temperature mediante il metodo di Eulero, determinato la soluzione analitica e verificato i risultati.
Ma come possiamo utilizzare questi dati all’interno di GX-TXT?
Potremmo immaginare di salvarli manualmente in un file CSV e successivamente importarli nella Session. Questo procedimento sarebbe possibile, ma richiederebbe operazioni aggiuntive e non sfrutterebbe pienamente l’integrazione fra Octave e il framework.
GX-TXT mette a disposizione un sistema più completo: le API pubbliche, accessibili attraverso lo Octave Script Bridge.
Che cos’è un’API?
La sigla API significa Application Programming Interface, ovvero interfaccia di programmazione delle applicazioni.
Un’API definisce un insieme di operazioni che un programma può richiedere a un altro componente software, senza dover conoscere necessariamente i dettagli della sua implementazione interna.
Possiamo fare un paragone con uno strumento di laboratorio.
Quando utilizziamo un multimetro digitale, possiamo scegliere una funzione di misura, impostare una portata e leggere il risultato. Non abbiamo bisogno di conoscere il firmware o i dettagli elettronici interni dello strumento per effettuare la misura.
Analogamente, quando utilizziamo un’API di GX-TXT, possiamo richiedere un’operazione scientifica al framework senza dover intervenire direttamente sul codice del CORE.
Per esempio, possiamo chiedere a GX-TXT di registrare una nuova serie di dati scientifici, specificandone i valori, le unità di misura e le informazioni sulla loro origine.
Sarà il framework a occuparsi della validazione, della memorizzazione e dell’aggiornamento dei registri della Session.
Perché utilizziamo le API pubbliche?
GX-TXT gestisce numerosi elementi: Session, Dataset, identificatori delle variabili, metadati, unità di misura, grafici e informazioni di provenienza dei risultati.
Questi elementi non sono semplici file indipendenti. Fanno parte di un sistema strutturato nel quale ogni dato possiede un’identità e può essere collegato alla propria origine.
Modificare direttamente i file interni del framework significherebbe aggirare i controlli previsti dal CORE e rischiare di produrre dati non coerenti con il resto della Session.
Le API pubbliche forniscono invece un’interfaccia documentata e controllata per eseguire le operazioni consentite.
Nel nostro corso adotteremo quindi una regola precisa: per interagire con GX-TXT utilizzeremo esclusivamente le API pubbliche documentate e disponibili nella versione di riferimento.
Lo Script Bridge: il collegamento tra Octave e GX-TXT
Lo Octave Script Bridge è il componente che permette agli script GNU Octave di comunicare con i servizi pubblici del framework.
Quando eseguiamo uno script registrato attraverso GX-TXT, il framework prepara l’ambiente di esecuzione e avvia automaticamente il Bridge.
Lo script può quindi utilizzare un insieme di funzioni speciali, riconoscibili dal prefisso gxtxt_, per scoprire e richiamare le API disponibili.
Non dobbiamo avviare manualmente il Bridge né gestirne direttamente la chiusura: queste operazioni appartengono al sistema di esecuzione di GX-TXT.
Lo Script Bridge mette a disposizione diverse funzioni. Per il nostro esperimento utilizziamo soprattutto:
| Funzione | Scopo |
|---|---|
gxtxt_api_require() | Dichiarare le API necessarie allo script. |
gxtxt_api_has_operation() | Controllare la disponibilità di una specifica operazione. |
gxtxt_api_describe() | Ottenere informazioni sul contratto pubblico di un’API. |
gxtxt_api_call() | Richiamare un’operazione pubblica del framework. |
gxtxt_context() | Consultare le informazioni relative all’esecuzione corrente. |
Vediamo come vengono utilizzate nel nostro script.
Dichiarare le API necessarie con gxtxt_api_require()
Il primo passaggio consiste nel dichiarare quali API pubbliche devono essere disponibili per completare l’esperimento.
gxtxt_api_require({'DATASET_PUBLICATION_API_v2@2:dataset_publication', ...
'PLOT_DATASET_API_v4@4:plot.dataset'});
Questa istruzione richiede due API:
DATASET_PUBLICATION_API_v2, utilizzata per pubblicare Dataset scientifici.PLOT_DATASET_API_v4, utilizzata per creare e gestire grafici scientifici.
Le stringhe non sono nomi scelti liberamente dal programmatore: sono dichiarazioni che identificano le API e i relativi contratti pubblici.
Prendiamo come esempio:
PLOT_DATASET_API_v4@4:plot.dataset
La parte PLOT_DATASET_API_v4 identifica l’API. Il numero dopo @ richiede la versione 4 del contratto, mentre plot.dataset identifica il provider pubblico atteso.
Il provider è il componente del framework che mette concretamente a disposizione quelle operazioni.
La funzione gxtxt_api_require() verifica che le dichiarazioni richieste siano soddisfatte. Se manca un requisito, viene generato un errore anziché continuare con un ambiente non compatibile.
Questa verifica è importante per la riproducibilità: uno script scientifico deve poter dichiarare chiaramente i servizi dai quali dipende.
Controllare le singole operazioni disponibili
La disponibilità generale di un’API non significa necessariamente che ogni operazione immaginabile sia utilizzabile dallo script.
Per questo motivo lo Script Bridge offre:
gxtxt_api_has_operation(api_id, operation)
La funzione riceve due argomenti: l’identificatore dell’API e il nome dell’operazione da verificare.
Per esempio:
available = gxtxt_api_has_operation( ...
'DATASET_PUBLICATION_API_v2', 'publish_series');
La variabile available conterrà un valore logico: vero se l’operazione è disponibile, falso altrimenti.
Nel nostro script eseguiamo questo controllo per tutte le operazioni necessarie:
required_operations = {
'DATASET_PUBLICATION_API_v2', 'publish_series';
'PLOT_DATASET_API_v4', 'new_spec';
'PLOT_DATASET_API_v4', 'upsert_plot';
'PLOT_DATASET_API_v4', 'render'
};
for index = 1:rows(required_operations)
api_id = required_operations{index, 1};
operation = required_operations{index, 2};
if !gxtxt_api_has_operation(api_id, operation)
error('Missing public GX-TXT operation: %s / %s', api_id, operation);
endif
endfor
Questo frammento ci permette anche di riprendere alcuni concetti studiati nella lezione.
required_operations è un cell array, una struttura di Octave che permette di raccogliere elementi come le stringhe di testo.
Ogni riga contiene una coppia di valori: l’identificatore di un’API e il nome di una delle sue operazioni.
La funzione rows() restituisce il numero di righe, mentre il ciclo for le esamina una alla volta.
Le parentesi graffe, come in required_operations{index, 1}, permettono di estrarre il contenuto di una cella.
Se una delle operazioni richieste non è disponibile, la struttura if genera un errore e interrompe l’esecuzione.
Abbiamo quindi un esempio concreto di come vettori di dati, cicli e condizioni possano collaborare per verificare le caratteristiche di un ambiente software.
Esaminare un’API con gxtxt_api_describe()
Prima di utilizzare un’API è utile conoscere quali operazioni espone e come devono essere richiamate.
GX-TXT permette di ottenere queste informazioni attraverso:
descriptor = gxtxt_api_describe(api_id);
Per esempio:
publication_api = gxtxt_api_describe('DATASET_PUBLICATION_API_v2');
plot_api = gxtxt_api_describe('PLOT_DATASET_API_v4');
Il risultato è una struttura Octave contenente informazioni sul contratto pubblico dell’API, sul provider e sulle operazioni disponibili, compresi i relativi argomenti e i contratti dei risultati.
Le strutture, chiamate struct in Octave, permettono di raccogliere più informazioni sotto un unico nome.
Per accedere a un campo utilizziamo il punto:
disp(publication_api.api_id);
In questo esempio chiediamo di visualizzare il campo api_id della struttura publication_api.
È importante non confondere il nome di un’API con quello di una singola operazione: l’API rappresenta il contratto complessivo, mentre le operazioni identificano le funzioni specifiche che il provider rende disponibili.
Il contesto dell’esecuzione: gxtxt_context()
Quando uno script viene eseguito attraverso GX-TXT, il framework associa alla sua esecuzione informazioni utili per identificarla.
Possiamo consultarle con:
context = gxtxt_context();
La funzione restituisce una struttura contenente informazioni sull’esecuzione corrente, come l’identificatore del job, il modulo che esegue lo script e gli eventuali Dataset di ingresso associati.
Nel nostro esperimento utilizziamo:
context = gxtxt_context();
if isfield(context, 'run_id')
fprintf('GX-TXT run ID: %s\n', context.run_id);
endif
La funzione Octave isfield() verifica se una struttura contiene un determinato campo.
La funzione fprintf() permette invece di visualizzare un messaggio formattato.
L’identificatore dell’esecuzione è utile per collegare i risultati ai log e alle informazioni conservate da GX-TXT.
Questo non significa che dobbiamo manipolare direttamente le directory interne della Session: il contesto ci fornisce informazioni pubbliche, mentre il framework continua a gestire autonomamente le proprie risorse.
Richiamare un’operazione pubblica con gxtxt_api_call()
Abbiamo imparato a verificare le API disponibili e a consultarne le caratteristiche.
Ora dobbiamo imparare a utilizzarle.
La funzione principale dello Script Bridge è:
result = gxtxt_api_call(api_id, operation, args);
Questa istruzione contiene tre argomenti fondamentali:
api_id: identifica l’API pubblica che vogliamo utilizzare.operation: indica l’operazione richiesta.args: contiene gli argomenti da trasmettere all’operazione, in un cell array e nell’ordine previsto dal contratto.
Il risultato viene restituito al programma Octave e può essere assegnato a una variabile.
Per esempio, per pubblicare una serie scientifica useremo:
result = gxtxt_api_call('DATASET_PUBLICATION_API_v2', ...
'publish_series', {request});
Osserviamo che request è racchiusa tra parentesi graffe.
Questo perché gxtxt_api_call() richiede che gli argomenti dell’operazione siano contenuti in un cell array.
Nel nostro esempio l’operazione publish_series riceve un solo argomento: una struttura contenente la descrizione della serie da pubblicare.
Il punto essenziale è che Octave non sta chiamando direttamente una funzione interna di GX-TXT. Sta effettuando una richiesta attraverso il contratto pubblico registrato nello Script Bridge.
Pubblicare i risultati con Dataset Publication API v2
Abbiamo due vettori contenenti le temperature calcolate, T_euler_K e T_exact_K, e un terzo vettore, time_min, che contiene i tempi ai quali le temperature si riferiscono.
Vogliamo trasformare queste informazioni in due Dataset scientifici registrati nella Session GX-TXT.
Per farlo utilizziamo DATASET_PUBLICATION_API_v2.
Questa API permette di pubblicare Dataset attraverso un procedimento gestito dal CORE: il framework valida le dichiarazioni scientifiche, seleziona il sistema di memorizzazione richiesto, controlla la coerenza dei dati e registra il risultato nella Session.
Per il nostro esperimento utilizziamo l’operazione publish_series, progettata per pubblicare una serie di coppie X/Y.
Descrivere le variabili scientifiche
Un Dataset non deve contenere soltanto numeri. Per essere utilizzato correttamente deve anche descrivere che cosa rappresentano quei numeri.
Iniziamo definendo la variabile del tempo:
x_schema = struct( ...
'variable_id', 'time_min', ...
'name', 'elapsed_time', ...
'display_label', 'Elapsed time', ...
'unit', 'min', ...
'semantic_role', 'time');
La funzione struct() costruisce una struttura associando nomi di campi ai rispettivi valori.
Esaminiamo i campi utilizzati:
| Campo | Significato |
|---|---|
variable_id | Identificatore stabile della variabile all’interno del Dataset. |
name | Nome della variabile scientifica. |
display_label | Etichetta destinata alla presentazione. |
unit | Unità di misura dichiarata. |
semantic_role | Ruolo scientifico attribuito alla variabile. |
In questo caso la variabile rappresenta il tempo trascorso, espresso in minuti.
Per le temperature definiamo strutture analoghe. Per esempio, per la soluzione di Eulero:
y_schema_euler = struct( ...
'variable_id', 'temperature_K', ...
'name', 'temperature_euler', ...
'display_label', 'Euler temperature', ...
'unit', 'K', ...
'semantic_role', 'temperature');
Il campo unit indica che i valori della temperatura sono espressi in kelvin.
Le informazioni dichiarate costituiscono parte della descrizione scientifica della variabile e saranno disponibili ai componenti GX-TXT che consultano il Dataset attraverso le API appropriate.
Preparare la richiesta di pubblicazione
Adesso dobbiamo costruire una struttura che descriva il Dataset da creare.
Nel nostro script utilizziamo:
euler_request = struct( ...
'dataset_id', 'lesson04.newton.euler', ...
'contract', 'table.generic', ...
'storage', 'csv', ...
'replace_owned', true, ...
'x_values', time_min, ...
'y_values', T_euler_K, ...
'x_variable', x_schema, ...
'y_variable', y_schema_euler, ...
'source', 'Newton cooling: explicit Euler model', ...
'provenance', 'GX-TXT dev111 P019; Lesson 04; dt=0.5 min; k=0.08 1/min');
Questa struttura è una vera e propria dichiarazione del risultato scientifico che vogliamo pubblicare.
Il campo dataset_id assegna un identificatore logico al nuovo Dataset.
Il campo contract dichiara il tipo di dati: in questo esempio utilizziamo table.generic, adatto a una semplice tabella scientifica.
Il campo storage richiede la memorizzazione in formato CSV attraverso il provider gestito dal framework.
I campi x_values e y_values contengono i due vettori numerici, che devono avere lo stesso numero di elementi.
x_variable e y_variable collegano invece i valori alle rispettive descrizioni scientifiche.
Il campo source descrive l’origine del risultato, mentre provenance conserva informazioni sul procedimento che lo ha generato.
Nel nostro caso registriamo la versione di GX-TXT utilizzata e i parametri fondamentali del calcolo.
Che cosa significa replace_owned?
Nella richiesta compare anche:
'replace_owned', true
Questa opzione consente di richiedere la sostituzione di un Dataset già esistente, ma soltanto quando il framework verifica che esso appartenga allo stesso produttore autorizzato e rispetti i vincoli della Publication API v2.
Non significa quindi che lo script possa sovrascrivere liberamente qualunque Dataset presente nella Session.
Se il Dataset appartiene a un altro produttore oppure non soddisfa i requisiti di sostituzione, GX-TXT rifiuta l’operazione.
Nel nostro esperimento questa opzione è utile perché permette di rieseguire lo script e aggiornare i risultati senza dover creare necessariamente un nuovo identificatore a ogni esecuzione.
Eseguire la pubblicazione
Una volta preparata la richiesta, possiamo richiamare il servizio pubblico:
result_euler = gxtxt_api_call( ...
'DATASET_PUBLICATION_API_v2', ...
'publish_series', ...
{euler_request});
L’operazione riceve la struttura euler_request, verifica il contenuto e richiede al framework di pubblicare la serie attraverso il normale sistema di Dataset Publication.
Se l’operazione termina correttamente, restituisce una struttura contenente le informazioni relative alla pubblicazione.
Se invece si verifica un problema, per esempio un identificatore non valido o una violazione delle regole di proprietà, l’API segnala un errore.
Il nostro programma non deve quindi modificare manualmente il registro dei Dataset e non deve calcolare o inserire autonomamente un hash di integrità.
Queste responsabilità appartengono al framework.
Pubblicare anche la soluzione analitica
Per la soluzione analitica utilizziamo la stessa API, ma con una seconda richiesta.
Il Dataset avrà l’identificatore:
lesson04.newton.analytical
Il vettore X sarà ancora time_min, mentre il vettore Y sarà T_exact_K.
La struttura descrittiva della variabile Y utilizzerà il nome temperature_analytical e l’etichetta Analytical temperature.
La pubblicazione viene quindi eseguita con:
result_exact = gxtxt_api_call( ...
'DATASET_PUBLICATION_API_v2', ...
'publish_series', ...
{exact_request});
Notiamo che i due Dataset possono utilizzare gli stessi identificatori locali time_min e temperature_K per le rispettive variabili.
Questo è possibile perché l’identità di una variabile GX-TXT è determinata dalla coppia Dataset ID + Variable ID.
Le variabili appartenenti ai due Dataset rimangono quindi distinte, anche quando possiedono lo stesso identificatore locale.
Il risultato: due Dataset scientifici nella Session GX-TXT
Al termine dell’esecuzione, GX-TXT ha registrato correttamente entrambi i Dataset:
lesson04.newton.euler, contenente la soluzione numerica.lesson04.newton.analytical, contenente la soluzione analitica.
Ciascun Dataset contiene 121 coppie di valori: tempo trascorso in minuti e temperatura in kelvin.
Durante il collaudo con GX-TXT DEV111-P019 abbiamo verificato che entrambi i Dataset erano presenti nella Session e che i loro valori potevano essere consultati attraverso le schede Table e Plot del Dataset Preview.
Questo risultato dimostra che lo script non ha soltanto eseguito correttamente i calcoli numerici: ha anche integrato i propri risultati nel sistema scientifico di GX-TXT attraverso le API pubbliche.
Riferimenti alle API utilizzate
Le interfacce descritte in questa sezione sono documentate nel repository ufficiale GX-TXT. Per garantire la riproducibilità dell’esperimento facciamo riferimento al commit 3f516f5b726183ea8f53041888bd67438ee050ee, corrispondente alla versione DEV111-P019 utilizzata durante il collaudo.
- Octave Script Bridge API v1 — Documentazione pubblica
- Dataset Publication API v2 — Documentazione pubblica
Abbiamo così imparato a scoprire le API disponibili, verificarne le operazioni e utilizzarle per pubblicare risultati scientifici dotati di identità, unità di misura e informazioni di provenienza.
Nel prossimo passaggio utilizzeremo Plot Dataset API v4 per costruire un grafico comparativo a partire dai due Dataset appena pubblicati, mantenendo il collegamento con le variabili scientifiche originali.
Creare grafici scientifici con Plot Dataset API v4
Abbiamo completato la simulazione del raffreddamento di Newton e pubblicato due Dataset nella Session GX-TXT.
Il primo contiene le temperature calcolate mediante il metodo numerico di Eulero; il secondo contiene quelle ottenute attraverso la soluzione analitica.
Ora vogliamo rappresentare entrambe le serie nello stesso grafico, per osservare quanto le due soluzioni siano vicine e come cambi la loro differenza nel tempo.
Per farlo utilizzeremo Plot Dataset API v4, l’interfaccia pubblica di GX-TXT dedicata alla gestione delle rappresentazioni grafiche scientifiche.
Perché non utilizziamo semplicemente plot() di Octave?
GNU Octave possiede già numerose funzioni grafiche. Per esempio, possiamo rappresentare due vettori utilizzando:
plot(time_min, T_euler_K);
Questa è una normale istruzione Octave, perfettamente valida per ottenere un grafico.
Nel nostro corso, tuttavia, vogliamo integrare i risultati nell’ambiente scientifico di GX-TXT.
Un grafico creato direttamente con le funzioni grafiche di Octave non diventa automaticamente un grafico scientifico persistente della Session GX-TXT.
Il framework dispone invece di un sistema di rappresentazione collegato ai Dataset canonici, che permette di gestire configurazioni, serie, varianti e operazioni di rendering attraverso API pubbliche.
Per questo utilizzeremo PLOT_DATASET_API_v4.
Il principio è semplice: non chiediamo a Octave di disegnare direttamente i dati, ma chiediamo a GX-TXT di rappresentare i Dataset che abbiamo pubblicato.
Separare i dati dalla loro rappresentazione
Prima di esaminare il codice, dobbiamo chiarire una distinzione fondamentale.
Un Dataset contiene informazioni scientifiche: valori numerici, unità di misura, identificatori delle variabili e metadati.
Un grafico descrive invece come questi dati devono essere rappresentati.
Per esempio, possiamo utilizzare lo stesso Dataset per produrre un grafico con linee continue, un grafico a punti oppure una rappresentazione con impostazioni differenti degli assi.
Queste modifiche non richiedono necessariamente di modificare i valori originali.
In GX-TXT la descrizione della rappresentazione è affidata a una struttura chiamata PlotSpec, abbreviazione di Plot Specification.
Un PlotSpec contiene le informazioni necessarie per costruire il grafico: quali Dataset utilizzare, quali variabili associare agli assi, quali serie rappresentare e come organizzare i pannelli.
Nel nostro esperimento utilizzeremo una specifica PlotSpec v2 attraverso l’API pubblica Plot Dataset v4. Il numero di versione del PlotSpec e quello dell’API sono distinti: identificano due contratti differenti.
Che cosa sono i pannelli e le serie?
Una serie rappresenta un insieme di dati da visualizzare, generalmente associando valori X e Y.
Nel nostro esperimento abbiamo due serie:
- La temperatura calcolata mediante Eulero.
- La temperatura calcolata mediante la soluzione analitica.
Un pannello è invece un’area del grafico nella quale vengono rappresentate una o più serie.
Un grafico può contenere un solo pannello oppure più pannelli, ciascuno con assi e serie differenti.
Noi utilizzeremo un unico pannello contenente entrambe le curve.
Questa scelta è importante: rappresentando le due temperature sugli stessi assi possiamo confrontarle direttamente, senza dover interpretare due grafici separati.
Prima operazione: creare una specifica con new_spec
Il primo passaggio consiste nel richiedere a GX-TXT una struttura PlotSpec iniziale.
Utilizziamo l’operazione pubblica new_spec:
spec = gxtxt_api_call('PLOT_DATASET_API_v4', 'new_spec', ...
{'plot.dataset.xy', ...
'Newton cooling: Euler versus analytical'});
Esaminiamo i componenti di questa istruzione.
gxtxt_api_call() è la funzione dello Script Bridge che abbiamo già incontrato.
Il primo argomento, PLOT_DATASET_API_v4, identifica l’API che vogliamo utilizzare.
Il secondo argomento, new_spec, indica l’operazione richiesta.
Il terzo argomento contiene un cell array con due elementi:
plot.dataset.xy: identifica il tipo di grafico scientifico bidimensionale che vogliamo costruire.Newton cooling: Euler versus analytical: definisce il titolo del grafico.
Il risultato dell’operazione viene assegnato alla variabile spec.
Questa variabile contiene una struttura Octave con la configurazione iniziale del grafico.
Non abbiamo ancora pubblicato o disegnato il grafico definitivo. Abbiamo soltanto preparato la struttura che ne descriverà le caratteristiche.
Accedere al pannello del grafico
La specifica contiene una collezione di pannelli.
Per recuperare il primo pannello utilizziamo:
panel = spec.panels{1};
Questa istruzione introduce due operazioni che abbiamo già incontrato studiando le strutture dati di Octave.
La notazione spec.panels permette di accedere al campo panels della struttura spec.
Le parentesi graffe {1} estraggono invece il contenuto del primo elemento del cell array.
La variabile panel conterrà quindi la struttura che descrive il nostro pannello grafico.
Possiamo immaginare il PlotSpec come un contenitore e il pannello come uno degli elementi presenti al suo interno.
Ora dobbiamo indicare quali curve visualizzare nel pannello.
Associare le serie ai Dataset canonici
Per rappresentare una curva non basta fornire un titolo. Dobbiamo identificare i dati che GX-TXT dovrà leggere.
Nel nostro caso conosciamo già gli identificatori dei due Dataset pubblicati:
lesson04.newton.euler
lesson04.newton.analytical
Conosciamo anche gli identificatori delle variabili:
time_min
temperature_K
Ricordiamo che gli identificatori delle variabili sono locali al rispettivo Dataset. La coppia formata da Dataset ID e Variable ID identifica una variabile scientifica specifica.
Possiamo quindi costruire due descrizioni delle serie, facendo riferimento direttamente alle identità canoniche.
La prima serie: il metodo di Eulero
La descrizione della prima serie è:
struct('id', 'euler', ...
'dataset_id', euler_request.dataset_id, ...
'x_variable_id', 'time_min', ...
'y_variable_id', 'temperature_K', ...
'axis', 'left')
Ogni campo ha uno scopo preciso.
| Campo | Significato |
|---|---|
id | Identificatore della serie all’interno del grafico. |
dataset_id | Dataset contenente i valori da rappresentare. |
x_variable_id | Variabile da associare all’asse orizzontale. |
y_variable_id | Variabile da associare all’asse verticale. |
axis | Asse Y al quale associare la serie. |
Nel nostro caso axis = left indica l’utilizzo dell’asse verticale sinistro.
La temperatura di Eulero sarà quindi rappresentata in funzione del tempo, utilizzando i valori contenuti nel Dataset canonico.
Osserviamo che non stiamo trasferendo nuovamente tutti i 121 valori numerici al sistema di plotting.
Stiamo invece specificando quale Dataset e quali variabili il framework deve utilizzare.
La seconda serie: la soluzione analitica
La seconda serie viene definita in modo analogo:
struct('id', 'analytical', ...
'dataset_id', exact_request.dataset_id, ...
'x_variable_id', 'time_min', ...
'y_variable_id', 'temperature_K', ...
'axis', 'left')
La differenza principale riguarda il campo dataset_id, che questa volta fa riferimento al Dataset contenente la soluzione analitica.
Entrambe le curve utilizzano il tempo sull’asse X e la temperatura sull’asse Y.
Poiché le grandezze hanno le stesse unità di misura, possiamo rappresentarle sul medesimo sistema di assi.
Inserire entrambe le serie nel pannello
Vediamo ora il frammento completo utilizzato nello script:
panel = spec.panels{1};
panel.series = {
struct('id', 'euler', ...
'dataset_id', euler_request.dataset_id, ...
'x_variable_id', 'time_min', ...
'y_variable_id', 'temperature_K', 'axis', 'left');
struct('id', 'analytical', ...
'dataset_id', exact_request.dataset_id, ...
'x_variable_id', 'time_min', ...
'y_variable_id', 'temperature_K', 'axis', 'left')
};
spec.panels = {panel};
La proprietà panel.series riceve un cell array contenente le due strutture.
Il punto e virgola tra i due elementi separa le righe del cell array: abbiamo quindi una collezione con due descrizioni indipendenti.
L’ultima istruzione è particolarmente importante:
spec.panels = {panel};
Con questa assegnazione inseriamo il pannello modificato nella specifica complessiva del grafico.
Perché dobbiamo farlo?
Perché abbiamo estratto il contenuto del pannello in una variabile separata, panel, e successivamente ne abbiamo modificato il campo series.
Riassegnando panel a spec.panels, ci assicuriamo che la specifica utilizzata nelle operazioni successive contenga effettivamente le due serie.
Ora la variabile spec descrive un grafico bidimensionale con un pannello e due curve associate ai Dataset scientifici.
Seconda operazione: creare o aggiornare il grafico con upsert_plot
Abbiamo preparato il PlotSpec. Adesso dobbiamo chiedere a GX-TXT di creare il grafico persistente nella Session.
Utilizziamo:
plot_ref = gxtxt_api_call('PLOT_DATASET_API_v4', 'upsert_plot', ...
{'lesson04.newton.cooling', spec});
Il nome upsert deriva dalla combinazione dei termini inglesi update e insert.
In informatica viene utilizzato per descrivere un’operazione che può creare un elemento oppure aggiornarne uno già esistente, secondo le regole previste dal sistema.
Nel nostro caso upsert_plot permette di creare o aggiornare un grafico persistente appartenente al modulo che esegue lo script.
Il primo argomento dell’operazione è:
lesson04.newton.cooling
Si tratta di una chiave stabile utilizzata per identificare il grafico nell’ambito del produttore.
Questa chiave permette al framework di riconoscere il grafico anche durante successive esecuzioni dello script, rispettando le regole di proprietà del provider.
Il secondo argomento è spec, cioè la struttura contenente la descrizione scientifica del grafico.
Il risultato dell’operazione viene assegnato alla variabile plot_ref.
Che cosa contiene plot_ref?
La variabile plot_ref contiene una struttura che identifica il grafico persistente creato o aggiornato da GX-TXT.
Questa struttura costituisce un riferimento pubblico che possiamo riutilizzare per richiedere altre operazioni sul grafico.
Per esempio, non è necessario conoscere il percorso fisico del file immagine che verrà prodotto.
È sufficiente utilizzare il riferimento restituito dall’API.
Questo è un principio ricorrente nell’architettura di GX-TXT: gli script lavorano attraverso identità e riferimenti gestiti dal framework, non attraverso percorsi privati di memorizzazione.
Terza operazione: generare il grafico con render
Abbiamo creato la descrizione del grafico e ottenuto il suo riferimento persistente.
Adesso possiamo richiederne la rappresentazione.
Lo script utilizza:
render_receipt = gxtxt_api_call('PLOT_DATASET_API_v4', ...
'render', {plot_ref});
L’operazione render richiede al framework di generare la rappresentazione grafica associata al riferimento fornito.
GX-TXT utilizza la configurazione del PlotSpec e consulta i Dataset indicati nelle descrizioni delle serie.
Il risultato è una rappresentazione scientifica delle due curve, gestita dal sistema di plotting.
La variabile render_receipt conserva il risultato restituito dall’operazione.
Lo script non chiama direttamente figure(), plot() o saveas() per produrre un’immagine indipendente.
La produzione e la gestione della rappresentazione sono affidate al framework attraverso la sua API pubblica.
Il risultato osservato durante il collaudo

Durante il collaudo della Lezione 04 con GX-TXT DEV111-P019 abbiamo verificato che il grafico era effettivamente presente nella sezione Plots della Session.
Il grafico, identificato nell’interfaccia come Plot p0001, contiene due curve corrispondenti alle soluzioni di Eulero e analitica.
La rappresentazione mostra l’evoluzione della temperatura nei 60 minuti simulati, con il tempo sull’asse orizzontale e la temperatura in kelvin sull’asse verticale.
Abbiamo verificato anche la generazione dell’immagine PNG e la possibilità di aprirla nella finestra dedicata di GX-TXT.
Il grafico è quindi un risultato reale dell’esperimento eseguito, non una rappresentazione costruita successivamente per illustrare l’articolo.
Interpretare il confronto tra le due curve
Osservando il grafico, notiamo che le due curve sono estremamente vicine.
Entrambe partono da 353,15 K, corrispondenti a 80 °C, e diminuiscono progressivamente avvicinandosi alla temperatura ambiente di 295,15 K, cioè 22 °C.
La curva ottenuta mediante Eulero presenta tuttavia una piccola differenza rispetto alla soluzione analitica.
Nel nostro caso, dopo il primo campione, l’approssimazione numerica si trova leggermente al di sotto della soluzione analitica: il metodo di Eulero esplicito produce un raffreddamento un po’ più rapido rispetto a quello descritto dalla formula esatta.
Non si tratta di un errore di programmazione. È una conseguenza dell’approssimazione introdotta dal metodo numerico.
Il metodo di Eulero considera la velocità di variazione all’inizio di ciascun intervallo e la utilizza per stimare la temperatura alla fine dell’intervallo.
Durante il raffreddamento, però, la velocità di variazione della temperatura diminuisce continuamente in valore assoluto.
Utilizzando la velocità iniziale per tutto l’intervallo, il metodo produce una diminuzione leggermente maggiore rispetto a quella effettiva.
Per questo motivo la curva numerica risulta leggermente inferiore a quella analitica.
Quanto è grande la differenza?
Al termine dei 60 minuti otteniamo:
| Metodo | Temperatura finale |
|---|---|
| Eulero esplicito | 22,43249 °C |
| Soluzione analitica | 22,47733 °C |
La differenza assoluta alla fine della simulazione è quindi circa 0,04484 K.
Il massimo errore assoluto calcolato lungo l’intera serie temporale è invece circa 0,434 K.
Questi due valori non coincidono perché l’errore cambia durante la simulazione: aumenta inizialmente e successivamente diminuisce mentre entrambe le temperature si avvicinano alla temperatura ambiente.
È importante precisare che questo errore rappresenta esclusivamente la differenza tra la soluzione numerica e quella analitica del modello considerato.
Non è un’incertezza sperimentale e non misura la capacità della legge di Newton di descrivere un particolare corpo reale.
Possiamo migliorare la precisione?
Uno dei modi più semplici per migliorare l’accuratezza del metodo di Eulero consiste nel ridurre l’intervallo temporale dt_min.
Per esempio, potremmo confrontare simulazioni con passi di:
- 1 minuto;
- 0,5 minuti;
- 0,25 minuti.
Diminuendo il passo temporale, aumentiamo il numero di iterazioni e generalmente riduciamo l’errore di discretizzazione per questo problema.
Non significa però che un passo infinitamente piccolo sia sempre la scelta migliore: nelle simulazioni numeriche bisogna considerare anche il costo computazionale, gli errori di arrotondamento e le caratteristiche del metodo utilizzato.
Per il nostro semplice esperimento, tuttavia, il confronto fra differenti passi temporali costituisce un esercizio utile per comprendere il rapporto tra risoluzione temporale e precisione.
Dataset Preview e Plot Dataset: due strumenti differenti
Durante il collaudo abbiamo utilizzato due modalità di visualizzazione dei risultati.
La prima è il Dataset Preview, disponibile nella sezione Dataset di GX-TXT.
Questo strumento permette di esplorare un Dataset, scegliere le variabili da rappresentare e consultarne i valori anche in forma tabellare.
Nel nostro esperimento lo abbiamo utilizzato per controllare separatamente i Dataset di Eulero e della soluzione analitica.
La seconda modalità è il grafico persistente creato tramite Plot Dataset API v4.
In questo caso abbiamo definito esplicitamente la composizione del grafico attraverso il PlotSpec, associandovi due serie provenienti da Dataset distinti.
Il risultato è un oggetto grafico gestito da GX-TXT, che può essere consultato nella sezione Plots della Session.
| Dataset Preview | Plot Dataset v4 |
|---|---|
| Esplorazione e consultazione dei dati | Creazione e gestione di grafici scientifici |
| Selezione interattiva delle variabili | Configurazione dichiarativa tramite PlotSpec |
| Visualizzazione tabellare e grafica | Gestione di serie, pannelli e rappresentazioni persistenti |
| Utile per controllare un Dataset | Utile per confrontare e presentare risultati scientifici |
I due strumenti non sono alternativi: rispondono a esigenze differenti e possono essere utilizzati insieme nello stesso flusso di lavoro.
Un esempio completo di integrazione grafica
Raccogliamo ora le istruzioni principali utilizzate nel nostro script:
spec = gxtxt_api_call('PLOT_DATASET_API_v4', 'new_spec', ...
{'plot.dataset.xy', ...
'Newton cooling: Euler versus analytical'});
panel = spec.panels{1};
panel.series = {
struct('id', 'euler', ...
'dataset_id', euler_request.dataset_id, ...
'x_variable_id', 'time_min', ...
'y_variable_id', 'temperature_K', 'axis', 'left');
struct('id', 'analytical', ...
'dataset_id', exact_request.dataset_id, ...
'x_variable_id', 'time_min', ...
'y_variable_id', 'temperature_K', 'axis', 'left')
};
spec.panels = {panel};
plot_ref = gxtxt_api_call('PLOT_DATASET_API_v4', 'upsert_plot', ...
{'lesson04.newton.cooling', spec});
render_receipt = gxtxt_api_call('PLOT_DATASET_API_v4', ...
'render', {plot_ref});
Il procedimento può essere riassunto in tre passaggi fondamentali:
- new_spec: prepara la descrizione iniziale del grafico.
- upsert_plot: crea o aggiorna il grafico persistente utilizzando il PlotSpec.
- render: richiede la generazione della rappresentazione grafica.
Tutte queste operazioni vengono eseguite attraverso gxtxt_api_call(), utilizzando esclusivamente l’interfaccia pubblica di GX-TXT.
Il framework mantiene così la gestione delle identità scientifiche e delle risorse grafiche, mentre lo script Octave si limita a descrivere il risultato che desidera ottenere.
Riferimento alla documentazione pubblica
Le operazioni presentate in questa sezione sono state verificate sui sorgenti GX-TXT DEV111-P019, commit 3f516f5b726183ea8f53041888bd67438ee050ee.
Il contratto pubblico è documentato in:
GX-TXT — Plot Dataset API v4, documentazione ufficiale su GitHub
Con questa operazione abbiamo completato l’integrazione della nostra simulazione nel framework: i risultati numerici sono conservati come Dataset canonici e il confronto tra i due metodi è disponibile come grafico scientifico nella Session.
Non abbiamo dovuto intervenire sui file interni del framework, né modificare il CORE, né gestire direttamente i percorsi di memorizzazione dei grafici.
Questo è uno degli obiettivi fondamentali del corso: imparare GNU Octave utilizzando le API pubbliche di GX-TXT per costruire procedure scientifiche riproducibili.
Eseguire e verificare l’esperimento in GX-TXT
Abbiamo esaminato il codice dello script, compreso il funzionamento delle strutture di controllo, delle funzioni Octave e delle API pubbliche utilizzate per pubblicare e rappresentare i risultati.
Ora vediamo come verificare concretamente il suo funzionamento all’interno di GX-TXT.
Un principio importante della programmazione scientifica è che un’esecuzione terminata senza errori non costituisce, da sola, una dimostrazione della correttezza dei risultati.
Occorre controllare anche i valori numerici, la coerenza delle unità di misura, la presenza dei Dataset pubblicati e il comportamento delle rappresentazioni grafiche.
Preparare la Session e registrare lo script
Per eseguire l’esperimento abbiamo creato una Session GX-TXT denominata lezione 04.
Attraverso lo Script Workspace abbiamo registrato il programma GNU Octave utilizzando i seguenti identificativi:
| Proprietà | Valore |
|---|---|
| Nome | Newton’s Law of Cooling — Euler Simulation |
| Script ID | octave.lesson04.newton_cooling |
| Linguaggio | GNU Octave |
| Session | lezione 04 |
| Framework | GX-TXT DEV111-P019 |
Lo script non richiede un Dataset di ingresso: tutti i valori necessari alla simulazione vengono generati direttamente dal programma, a partire dai parametri iniziali.
Una volta salvato lo script nel registro della Session, possiamo eseguirlo attraverso il normale sistema di Script Jobs di GX-TXT.
Il framework si occupa di avviare l’interprete Octave, predisporre lo Script Bridge, controllare le dipendenze dichiarate e registrare le informazioni relative all’esecuzione.
Controllare lo stato dell’esecuzione

GX-TXT permette di consultare le esecuzioni attraverso la finestra Script Jobs.
Questa interfaccia mostra informazioni utili come lo stato del Job, lo script registrato, la revisione utilizzata, gli orari di esecuzione e i relativi log.
Durante il nostro collaudo abbiamo ottenuto uno stato finale COMPLETED, con un tempo di esecuzione di circa 6,7 secondi.
Il log riporta i risultati numerici principali e conferma che le API pubbliche necessarie erano disponibili.
Vediamo una parte significativa dell’output:
GX-TXT GNU Octave Course - Lesson 04
Euler steps: 120; samples: 121
Final Euler temperature: 22.43248989 deg C
Final analytical temperature: 22.47732533 deg C
Maximum absolute error: 0.43399801 K
Threshold reached after: 50.00 min (100 steps)
Final state: Within ambient tolerance
Available public APIs: DATASET_PUBLICATION_API_v2 and PLOT_DATASET_API_v4
Esaminiamo il significato dei risultati.
Interpretare i risultati numerici
Il programma ha effettuato 120 aggiornamenti numerici, producendo 121 campioni di temperatura, compreso quello iniziale.
Dopo 60 minuti, il metodo di Eulero restituisce una temperatura di circa 22,43249 °C, mentre la soluzione analitica fornisce circa 22,47733 °C.
L’errore massimo assoluto lungo l’intera simulazione è di circa 0,434 K.
Il ciclo while ha inoltre determinato che, secondo l’approssimazione numerica adottata, il corpo raggiunge una differenza dalla temperatura ambiente non superiore a 1 K dopo 100 passaggi, corrispondenti a 50 minuti.
Possiamo riassumere i risultati nella seguente tabella:
| Grandezza | Risultato |
|---|---|
| Durata della simulazione | 60 minuti |
| Passo temporale | 0,5 minuti |
| Passaggi del metodo di Eulero | 120 |
| Campioni di temperatura | 121 |
| Temperatura finale, Eulero | 22,43249 °C |
| Temperatura finale, soluzione analitica | 22,47733 °C |
| Errore massimo assoluto | 0,433998 K |
| Tempo di raggiungimento della soglia | 50 minuti |
Questi valori sono coerenti con il comportamento previsto dalla legge di raffreddamento di Newton e con il metodo numerico utilizzato.
È però importante ricordare che stiamo verificando la soluzione di un modello matematico, non confrontando la simulazione con misure sperimentali reali.
Verificare i Dataset pubblicati nella Session
Il completamento del Job e la correttezza dei risultati numerici rappresentano soltanto una parte della verifica.
Il nostro script deve anche pubblicare correttamente i dati attraverso DATASET_PUBLICATION_API_v2.
Per controllare questo passaggio apriamo la sezione Dataset di GX-TXT e osserviamo l’elenco dei Dataset registrati nella Session.
Nel nostro collaudo troviamo entrambi i risultati:
lesson04.newton.eulerlesson04.newton.analytical
Entrambi risultano associati al modulo produttore analysis.octave_script.
Selezionando ciascun Dataset possiamo consultare i valori attraverso le schede di anteprima grafica e tabellare.
Controllare la tabella dei valori


La scheda Table permette di esaminare direttamente i campioni pubblicati.
Per entrambi i Dataset troviamo una variabile temporale, espressa in minuti, e una variabile di temperatura, espressa in kelvin.
Il primo campione corrisponde al tempo zero e alla temperatura iniziale di 353,15 K.
Nel Dataset di Eulero, il secondo campione corrisponde al tempo 0,5 minuti e contiene una temperatura di 350,83 K.
Il valore coincide con quello che avevamo calcolato manualmente quando abbiamo introdotto il metodo numerico.
Nel Dataset analitico, invece, il secondo campione è leggermente differente, perché viene calcolato direttamente dalla soluzione esponenziale della legge di Newton.
Questa verifica è particolarmente utile: ci permette di confrontare un risultato ottenuto manualmente con quello effettivamente prodotto e pubblicato dallo script.
Controllare l’anteprima grafica
La scheda Plot del Dataset Preview permette di osservare separatamente l’andamento delle due serie.
In entrambi i casi la temperatura diminuisce progressivamente e si avvicina alla temperatura ambiente.
La visualizzazione conferma qualitativamente il comportamento atteso: il raffreddamento è più rapido all’inizio e rallenta man mano che diminuisce la differenza di temperatura.
Abbiamo così verificato che i due Dataset sono registrati, leggibili e utilizzabili dagli strumenti scientifici di GX-TXT.
Che cosa abbiamo realmente verificato?
Al termine del collaudo possiamo distinguere diversi livelli di verifica:
| Verifica | Esito |
|---|---|
| Esecuzione dello script Octave | Superata |
| Calcolo delle temperature | Superata |
| Terminazione dei cicli | Superata |
| Disponibilità delle API pubbliche richieste | Superata |
| Pubblicazione dei due Dataset | Superata |
| Consultazione tabellare dei risultati | Superata |
| Dataset Preview | Superata |
| Creazione e rendering del grafico Plot Dataset v4 | Superata |
Questi controlli documentano il funzionamento dell’esperimento nella configurazione effettivamente utilizzata.
Non rappresentano una validazione generale di tutte le funzionalità GX-TXT o di tutte le possibili combinazioni di parametri del modello.
Il codice è stato verificato con GX-TXT 0.9.8.10-dev-111, patch P019, utilizzando come riferimento il commit GitHub 3f516f5b726183ea8f53041888bd67438ee050ee.
Abbiamo quindi completato il percorso scientifico della lezione: dalla formulazione matematica del problema alla sua implementazione in GNU Octave, fino alla verifica dei risultati numerici e alla loro integrazione nella Session attraverso le API pubbliche del framework.
Materiali del corso e codice sorgente
L’esperimento presentato in questa lezione è disponibile come script GNU Octave, accompagnato da due documenti di approfondimento in lingua inglese.
Il codice è stato sviluppato facendo riferimento esclusivamente alle API pubbliche di GX-TXT DEV111-P019 e collaudato all’interno di una Session del framework.
I materiali disponibili sono:
- Script GNU Octave:
GXTXT_Octave_L04_DEV111_P019_v1.0.1.m— contiene il programma completo per la simulazione, la pubblicazione dei Dataset e la creazione del grafico comparativo. - Teaching Notes:
Lesson_04_Teaching_Notes_DEV111_P019— contiene le note didattiche, i concetti affrontati e i riferimenti alle API. - Experiment Objectives:
Lesson_04_Experiment_Objectives_DEV111_P019— descrive gli obiettivi dell’esperimento, i parametri utilizzati e i criteri di verifica dei risultati.
I due documenti Markdown possono essere consultati con un normale editor di testo oppure con un visualizzatore Markdown.
Lo script è distribuito con licenza GNU GPL v3 o successiva. I commenti interni sono scritti in inglese, così da mantenere il codice utilizzabile anche da lettori internazionali.
Come riprodurre l’esperimento
Per ripetere la simulazione è necessario disporre di un’installazione GX-TXT compatibile con le API utilizzate e di un ambiente GNU Octave correttamente configurato.
- Aprire GX-TXT e creare o selezionare una Session di lavoro.
- Aprire lo Script Workspace dedicato a GNU Octave.
- Importare il file
GXTXT_Octave_L04_DEV111_P019_v1.0.1.m. - Registrare lo script nella Session e verificare che le API richieste siano disponibili.
- Salvare ed eseguire lo script attraverso il sistema Script Jobs.
- Controllare il log dell’esecuzione.
- Aprire la sezione Dataset e verificare la presenza delle due serie pubblicate.
- Aprire la sezione Plots e consultare il grafico comparativo.
Non è necessario importare preventivamente dati sperimentali: lo script genera autonomamente le temperature numeriche e analitiche.
È consigliabile effettuare le prime prove in una Session dedicata, separata dai propri dati di laboratorio.
Esercizi — Mettiamo alla prova quello che abbiamo imparato
La programmazione si impara soprattutto modificando ed eseguendo i programmi.
Gli esercizi seguenti permettono di approfondire progressivamente le strutture condizionali, i cicli e le funzioni utilizzate durante la lezione.
Prima di iniziare, conserva una copia dello script originale oppure crea una nuova Session GX-TXT per le esercitazioni. In questo modo potrai sperimentare liberamente senza sostituire i risultati del collaudo iniziale.
Esercizio 1 — Modificare la temperatura iniziale
Nel nostro esperimento abbiamo simulato il raffreddamento di un corpo inizialmente a 80 °C.
Modifica il programma per simulare un corpo che parte invece da 60 °C, mantenendo invariati tutti gli altri parametri.
Individua l’istruzione:
T_initial_K = 80 + 273.15;
Modificala opportunamente, esegui lo script e confronta i risultati.
Domande: la temperatura finale dopo 60 minuti sarà maggiore o minore rispetto all’esperimento originale? Il tempo necessario per raggiungere la soglia di 1 K sarà lo stesso?
Prima di eseguire il programma, prova a formulare una previsione motivata.
Esercizio 2 — Modificare il passo temporale
Ripeti la simulazione utilizzando tre differenti intervalli temporali:
dt_min = 1.0;dt_min = 0.5;dt_min = 0.25;
Mantieni invariati la durata complessiva, le temperature iniziali e il coefficiente di raffreddamento.
Ricorda di adeguare anche il limite massimo delle iterazioni del ciclo while. Riducendo il passo temporale, infatti, aumenta il numero di passaggi necessari per raggiungere una determinata temperatura.
Per questo esercizio puoi utilizzare:
max_threshold_steps = ceil(duration_min / dt_min);
La funzione ceil() arrotonda un numero all’intero superiore. In questo modo il ciclo dispone di un numero massimo di aggiornamenti coerente con la durata della simulazione.
Registra per ogni prova il numero di campioni, la temperatura finale calcolata con Eulero e il massimo errore assoluto rispetto alla soluzione analitica.
Domanda: come cambia l’errore numerico quando diminuiamo il passo temporale?
Questo esercizio permette di introdurre sperimentalmente il concetto di convergenza di un metodo numerico.
Esercizio 3 — Modificare la condizione di arresto
Nel nostro esperimento il ciclo while termina quando la differenza dalla temperatura ambiente diventa minore o uguale a 1 K.
Prova a modificare:
threshold_K = 1.0;
utilizzando successivamente i valori 2 K, 0,5 K e 0,1 K.
Per ciascuna prova osserva il numero di iterazioni effettuate e il tempo necessario per raggiungere la soglia.
Attenzione: per soglie particolarmente piccole il numero massimo di iterazioni potrebbe non essere sufficiente.
Se questo accade, il programma deve segnalare correttamente che la condizione non è stata raggiunta entro il limite previsto.
Domanda: perché richiedere una differenza di temperatura sempre più piccola comporta un numero crescente di iterazioni?
Esercizio 4 — Scrivere una nuova funzione
Ora proviamo a introdurre una funzione che non è presente nello script originale.
Scrivi una funzione chiamata temperature_difference(), capace di ricevere due temperature e restituire il valore assoluto della loro differenza.
La dichiarazione dovrà essere:
function difference = temperature_difference(T1, T2)
Completa il corpo della funzione utilizzando abs() e concludi la definizione con endfunction.
Successivamente modifica il ciclo while per utilizzare la nuova funzione al posto del calcolo diretto:
abs(T_threshold_K - T_ambient_K)
Obiettivo: verificare che il comportamento dello script rimanga invariato anche dopo aver sostituito un’espressione con una funzione riutilizzabile.
Questo esercizio introduce un principio importante della progettazione software: possiamo migliorare l’organizzazione del codice senza modificarne il comportamento scientifico.
Esercizio 5 — Sperimentare con le condizioni
Modifica la funzione classify_state() in modo che distingua quattro stati invece di tre.
Per esempio, potresti utilizzare le seguenti categorie:
- Differenza inferiore o uguale alla tolleranza: Within ambient tolerance.
- Differenza superiore alla tolleranza ma non superiore al doppio: Very close to ambient.
- Differenza superiore al doppio ma non superiore a cinque volte la tolleranza: Approaching ambient temperature.
- Differenza superiore a cinque volte la tolleranza: Cooling in progress.
Utilizza if, elseif ed else per implementare questa classificazione.
Verifica poi il comportamento della funzione con differenti temperature.
Domanda: perché l’ordine delle condizioni è importante? Che cosa accadrebbe se verificassimo prima la condizione più generale e successivamente quelle più restrittive?
Considerazioni finali
In questa lezione siamo partiti da un semplice problema fisico per introdurre alcuni degli strumenti fondamentali della programmazione GNU Octave.
Abbiamo imparato a utilizzare if, elseif ed else per prendere decisioni; il ciclo for per ripetere un numero definito di operazioni; il ciclo while per eseguire un calcolo fino al raggiungimento di una condizione.
Abbiamo inoltre utilizzato break e continue per controllare il flusso delle iterazioni e abbiamo imparato a suddividere il programma in funzioni riutilizzabili.
Il risultato non è stato soltanto un esercizio di programmazione.
Abbiamo costruito una simulazione numerica completa, confrontato il metodo di Eulero con una soluzione analitica e verificato l’errore introdotto dall’approssimazione.
Attraverso le API pubbliche GX-TXT abbiamo infine trasformato i risultati in due Dataset canonici e creato un grafico scientifico persistente, senza modificare direttamente i file interni del framework.
Questo è l’approccio che continueremo a utilizzare durante il corso: imparare GNU Octave attraverso esperimenti scientifici riproducibili, sfruttando le funzionalità pubbliche di GX-TXT per gestire dati, calcoli e rappresentazioni grafiche.
Versione del framework e riferimenti
L’esperimento è stato eseguito e verificato utilizzando la seguente configurazione:
| Framework | GX-TXT |
|---|---|
| Versione | 0.9.8.10-dev-111 |
| Patch | P019 |
| Branch GitHub | maintenance/dev111 |
| Commit di riferimento | 3f516f5b726183ea8f53041888bd67438ee050ee |
| Linguaggio | GNU Octave |
| Script del corso | Lezione 04, versione 1.0.1 |
| Esito del collaudo | Completato con successo |
Il codice sorgente ufficiale del framework è disponibile nel repository GX-TXT su GitHub.
Per questa lezione è stato utilizzato il commit DEV111-P019 del 25 ottobre 2026.
Le API pubbliche utilizzate sono documentate nei manuali dello Script Bridge Octave, della Dataset Publication API v2 e della Plot Dataset API v4.
Nota sulla compatibilità: GX-TXT è un progetto in evoluzione. Le interfacce e le procedure mostrate nell’articolo si riferiscono alla versione effettivamente utilizzata per il collaudo. Versioni successive del framework potrebbero introdurre modifiche o nuove funzionalità.