Il programma è una modifica di quello che abbiamo già visto per il calcolo dell'entropia della stessa sostanza. Qui viene aggiunta una funzione per il calcolo dell'entalpia che sfrutta ancora i dati sul calore specifico, ricordando che, a pressione costante, $dH=C_P\,dT$ e $dS=C_P/T\,dT$.
Come dato aggiuntivo abbiamo anche bisogno dell'entalpia di formazione del piropo dagli elementi, in condizioni standard ($H_0$).
Tutti i dati preventivi necessari sono conservati nella variabile reg che è un'istanza della classe fpar:
class fpar():
def __init__(self):
self.flag=False
self.ex=(0,1,-1,2, -2,0.5,-0.5)
self.size=len(self.ex)
self.par=np.ones(self.size)
self.h0=-6281969.575
self.s0=269.5
self.g0=self.h0-298.15*self.s0
self.minT=10.
def set(self,par):
self.par=par
def set_h0(self,h):
self.h0=h
def set_s0(self,s):
self.s0=s
def set_g0(self):
self.g0=self.h0-298.15*self.s0
def out(self):
print("Entalpia di formazione %12.2f J/mole" % self.h0)
print("Entropia di stato standard %9.2f J/mole K" % self.s0)
print("Energia libera di Gibbs di stato standard %9.2f J/mole" %\
self.g0)
print("\nCoefficienti del polinomio che esprime Cp in funzione")
print("di T, determinati per 'best fit' dai dati sperimentali")
index=0
for ip in self.par:
print("parameter %i, value: %6.4e" % (index, self.par[index]))
index=index+1
La variabile reg viene creata con
reg=fpar()
Una volta creata, reg contiene
reg.ex=(0,1,-1,2, -2,0.5,-0.5)
che produrrà la funzioneLa classe fpar (e quindi la sua istanza reg) comprende anche delle funzioni che consento di operare sui valori della variabili che conserva; per esempio
def set(self,par):
self.par=par
quando invocata con
reg.set(opt)
salva la lista opt nella variabile reg.par
Queste funzioni definite nella classe fpar vengono chiamate metodi della classe. reg.set è dunque un metodo della classe reg, istanza della classe fpar.
Ci si prenda il tempo necessario per studiarsi la classe fpar. Per approfondire l'argomento esiste moltissimo materiale in rete, anche in italiano. Per esempio, trovate qui un testo esplicativo sula sintassi e sul funzionamento delle classi. Qualche nota di approfondimento è anche scritta qui, al termine di questo notebook.
Lanciamo adesso il magic command %matplotlib inline
per avere la grafica inline in questo notebook e
%run gibbs.py
per lanciare il programma
%matplotlib inline
%run gibbs.py
Visualizziamo i dati salvati in reg, con il metodo out
di reg
reg.out()
Le variabili conservate in reg possono anche essere visualizzate (usate e anche assegnate) direttamente, semplicemente invocandole; per esempio:
reg.ex
Per ottenere l'energia libera di Gibbs alla temperature T si può partire chiamando direttamente la funzione gibbs
, specificando la temperatura e il parametro prt=True
(default prt=True
)
def gibbs(T,prt=False):
if not reg.flag:
print("Eseguo preventivamente la funzione start")
start()
enth_T=entalpia(T)+reg.h0 # Calcolo di H(T)
ent_T=entropia(T) # Calcolo di S(T)
gibbs_T=enth_T-T*ent_T # Calcolo di G(T)=H(T)-T*S(T)
DG=gibbs_T-reg.g0 # Calcolo di Delta_G = G(T) - G0
if prt:
print("Delta G(T): %8.2f J/mole; T: %5.1f K" % (DG,T))
return
return DG # restituzione di Delta_G
Poichè la funzione è chiamata senza aver preventivamente chiamato la funzione start
, che esegue il fit del $C_P(T)$ e calcola l'entropia di stato standard, gibbs
chiama preventivamente start
. Le successive chiamate di gibbs
non necessitano più della chiamata start
. Questo meccanismo è realizzato dalle tre righe di codice
if not reg.flag:
print("Eseguo preventivamente la funzione start")
start()
La variabile reg.flag
è inizializzata a False
all'inizio del programma; not reg.flag
è dunque True
e il codice scritto entro la condizione if
è eseguito: viene chiamata la funzione start
che, tra le altre cose, cambia il valore di reg.flag
a True
, in modo che la volta successiva l'espressione
not reg.flag=False
sia False
e il codice della condizione if
non più eseguito.
La funzione start è:
def start():
reg.flag=True # switch di reg.flag a True
T=500 # T max per il plot di Cp(T)
fit(prt=False) # best fit della funzione Cp sui dati sperimentali di Cp
check_cp() # plot di Cp(T)
plot_entropy(T) # calcolo dell'entropia e plot di S(T)
ent_st=entropia(298.15) # entropia nello stato standard S0
reg.set_s0(ent_st) # assegnazione di S0 a alla variabile reg.s0, attraverso il metodo reg.set_s0
reg.set_g0() # assegnazione di G0 a reg.g0
print("\nEntropia di stato standard: %6.2f J/mole K" % ent_st)
print("Energia libera di stato standard: %9.2f J/mole" % reg.g0)
gibbs(500,prt=True)
start
esegue il fit di $C_P$, calcola l'entropia di stato standard $S_0$ e l'energia libera nello stato standard $G_0$, a partire dall'entalpia di formazione allo stato standard $H_0$ (contenuta nella variabile reg.h0
):
$$G_0=H_0 - T_0\,S_0$$
dove $T_0=298.15$. Attraverso i due integrali
$$\int_{H_0}^{H} dH = \int_{T_0}^{T} C_PdT \rightarrow H(T)=H_0 + \int_{T_0}^{T} C_PdT$$e
$$\int_{0}^{S} dS = \int_{0}^{T} \frac{C_P}{T}dT \rightarrow S(T)=\int_{0}^{T} \frac{C_P}{T}dT$$
viene quindi calcolata $G(T)=H(T)-TS(T)$ e restituito il valore $\Delta G=G(T)-G_0$
Poichè, a questo punto, la funzione fit è stata eseguita, se invochiamo il metodo reg.out visualizziamo le nuove variabili reg.par aggiornate (anche s0 e g0 sono aggiornate):
reg.out()
La successiva chiamata di gibbs non esegue più alcun fit (perchè adesso reg.flag=True
):
print(reg.flag)
gibbs(600,prt=True)
Il plot della funzione $\Delta G = G(T)-G_0$ in funzione di $T$ viene quindi effettuato chiamando la funzione gibbs_plot che ha codice:
def gibbs_plot(T):
if T < 298.15:
print("La temperatura deve essere superiore a 298.15 K")
return
T_list=np.linspace(298.15, T, 50) # T_list è una lista di 50 valori di temperatura da 50 a T K
gibbs_list=np.array([]) # lista (array) gibbs_list inizialiazzata (vuota)
for it in T_list: # ciclo su tutti i valori in T_list
ig=gibbs(it) # calcolo energia G alla temperatura "it"
gibbs_list=np.append(gibbs_list,ig) # i valori di energia libera (ig) vengono salvati in gibbs_list
plt.figure() # sezione grafica
plt.plot(T_list,gibbs_list)
plt.xlabel("T (K)")
plt.ylabel("Delta G (J/mole)")
plt.title("Delta G = G(T) - G(298.15)")
plt.show()
Poniamo la T massima per cui fare il plot a 600 K:
gibbs_plot(600)
Nella scrittura di un programma python occorre fare attenzione all'assegnazione e all'uso che si fa delle variabili. Ogni variabile è definita in un certo contesto e può essere vista o non vista in altri contesti (funzioni) dello stesso programma.
Come regola generale, una variabile definita e assegnata nel corpo principale del programma viene passata a tutte le funzioni definite nello stesso programma. Si veda in proposito il piccolo programma che segue
a=10
print("Stampa iniziale di 'a': %i" % a)
def fun():
print("Stampa di 'a' all'interno della funzione: %i" % a)
# chiamata alla funzione fun che stampa "a" a cui la funzione medesima può accedere
fun()
Modifichiamo adesso il programma in modo che venga definita, assegnata e usata una variabile b all'interno della funzione fun
a=10
print("Stampa iniziale di a: %i" % a)
def fun():
b=5
print("Stampa di 'a' all'interno della funzione: %i" % a)
c=a+b
print("Stampa di 'b' e 'c=a+b': %i, %i" % (b, c))
# chiamata alla funzione fun che stampa "a" a cui la funzione medesima può accedere
fun()
Tuttavia, se proviamo a fare stampare la variabile b, con un print(b)
al di fuori della funzione fun (e dopo che fun sia stata chiamata) otteniamo un errore: name 'b' is not defined
a=10
print("Stampa iniziale di 'a': %i" % a)
def fun():
b=5
print("Stampa di 'a' all'interno della funzione: %i" % a)
c=a+b
print("Stampa di 'b' e 'c=a+b': %i, %i" % (b, c))
# chiamata alla funzione fun che stampa "a" a cui la funzione medesima può accedere
fun()
print(b)
Questo perchè b è definita localmente all'interno della funzione fun e non è accessibile al di fuori di quella funzione.
Interessante il comportamento che vediamo nel programmino seguente:
a=10
print("Stampa iniziale di 'a': %i" % a)
def fun():
a=5
print("Stampa di 'a' all'interno della funzione: %i" % a)
# chiamata alla funzione fun che stampa "a" a cui la funzione medesima può accedere
fun()
print("Nuova stampa di 'a' al di fuori della funzione: %i" % a)
Qui la variabile a è riassegnata nella funzione fun e stampata (valore 5); ma quando si esce da fun, e si richiede nuovamente il valore di a, si riottiene il valore originale (10). Questo perchè la variabile a definita nel programma principale (l'ambiente) non è la stessa variabile locale a definita all'interno di fun seppur abbia lo stesso nome; a globale e a locale sono in effetti due variabili distinte.
Se vogliamo modificare la variabile a in fun e conservarla anche al di fuori della funzione, dobbiamo definirla come global
all'interno della funzione:
a=10
print("Stampa iniziale di 'a': %i" % a)
def fun():
global a, b
a=5
b=20
print("Stampa di 'a' all'interno della funzione: %i" % a)
# chiamata alla funzione fun che stampa "a" a cui la funzione medesima può accedere
fun()
print("Nuova stampa di 'a' al di fuori della funzione: %i" % a)
print("Stampa di 'b' al di fuori della funzione: %i" % b)
Come vediamo, l'ultima stampa di a dopo l'esecuzione di fun ci dice che effettivamente la variabile è stata modificata. Non solo, ma definendo come global anche b, all'interno della funzione, il suo valore viene conservato e reso disponibile all'esterno della funzione medesima.
Questo comportamento delle variabile va sotto il nome di scoping: lo scope di una variabile è l'ambiente entro il quale risulta definita: le variabili definite nell'ambiente principale hanno scoping globale, mentre quelle nelle funzioni hanno scoping locale e, in generale, non hanno nessuna connessione con quelle globali dell'ambiente esterno, anche se hanno (apparentemente) lo stesso nome. All'interno di una funzione, una variabile locale può essere resa globale usando esplicitamente la direttiva global
Ora, tutti gli sviluppatori del linguaggio python sconsigliano assolutamente di scrivere codice in cui ci siano funzioni che modifichino i valori di variabili globali! Questo perchè, in programmi lunghi, si perde molto rapidamente traccia di chi modifica cosa, diventa facile fare errori (bug) ed è difficile scovarli tutte le volte che questi programmi mostrano problemi. In un programma scritto bene non dovrebbero esserci direttive global
all'interno delle funzioni.
C'è anzi il rischio che ciò che sia oggi solo sconsigliato (deprecated nel gergo informatico) diventi vietato in successive release del sistema, per cui codice scritto oggi con pratiche deprecated, in futuro potrebbe non funzionare più negli ambienti upgraded.
La soluzione esiste e sta nella definizione delle classi. Si esamini il codice seguente (e i commenti al suo interno):
class var():
def __init__(self,vv): # Inizializzazione della variabile 'a'
self.a=vv
def set_a(self,vv): # metodo per modificare la variabile
self.a=vv
def print(self): # metodo per stampare la variabile
print("La variabile vale %i" % self.a)
v=var(10) # vengono create due istanze della classe var
w=var(5)
print("Stampe iniziali delle variabili 'v.a' e 'w.a':")
v.print() # Viene usato il metodo print delle due istanze v e w per
w.print() # stampare le variabili corrispondenti v.a e w.a
# Si noti che, non solo v.a e w.a sono variabili diverse,
# ma diversi sono anche i metodi v.print e w.print
def fun(): # semplice funzione che modifica v.a attraverso il metodo v.set_a
v.set_a(20)
print("\nStampa della variabile 'v.a' all'interno della funzione")
v.print()
fun() # chiamata della funzione fun
print("\nStampa di 'v.a' all'esterno della funzione (che l'ha modificata)")
v.print()
Come si vede, la funzione fun, usando il metodo v.set_a, è stata in grado di modificare globalmente il valore della variabile v.a, senza usare alcuna direttiva global
.
Da notare che le variabili delle classi si possono modificare direttamente senza passare dai metodi (come set_a), come fatto nel programmino che segue (che è una modifica del precedente) in cui fun, che accetta adesso anche un argomento vv, modifica direttamente v.a con un'assegnazione diretta v.a=vv
.
class var():
def __init__(self,vv): # Inizializzazione della variabile 'a'
self.a=vv
def set_a(self,vv): # metodo per modificare la variabile
self.a=vv
def print(self): # metodo per stampare la variabile
print("La variabile vale %i" % self.a)
v=var(10) # vengono create due istanze della classe var
w=var(5)
print("Stampe iniziali delle variabili 'v.a' e 'w.a':")
v.print() # Viene usato il metodo print delle due istanze v e w per
w.print() # stampare le variabili corrispondenti v.a e w.a
# Si noti che, non solo v.a e w.a sono variabili diverse,
# ma diversi sono anche i metodi v.print e w.print
def fun(vv): # semplice funzione che modifica v.a attraverso il metodo v.set_a
v.a=vv
print("\nStampa della variabile 'v.a' all'interno della funzione")
v.print()
fun(20) # chiamata della funzione fun con argomento vv=20
print("\nStampa di 'v.a' all'esterno della funzione (che l'ha modificata)")
v.print()
Questa pratica, per quanto sia possibile, viene pure sconsigliata (ma non come nel caso dell'uso di global), caldeggiando invece la definizione e l'uso di metodi appositi definiti all'interno della classe (set_a). Questa, in effetti, è la filosofia delle tecniche di programmazione ad oggetti in cui le classi vengono costruite per conservare dati e funzioni che possono accedere a quei dati: quelle stesse funzioni fanno da interfaccia per tutte le altre funzioni del programma che devono accedere a quegli stessi dati. In alcuni linguaggi come il C++, non è possibile accedere alle variabili delle classi senza passare dai metodi che fanno da interfaccia.