8.3.1 Premessa: dati necessari
Le funzioni che seguono hanno solitamente bisogno di informazoni riguardo la catena per funzionare. Queste vengono sempre fornite attraverso una struttura chiamata "dizionario", nella quale le varie in- formazioni vengono conservate associandole ad una chiave (un nome) per poter poi esser facilmente richiamate.
Il dizionario standard, che potenzialmente è in grado di contenere tutte le informazioni riguardo una catena, è così concepito:
dati_trave =
• ’lunghezza’: lunghezza della catena [m],
• ’modulo_young’: modulo elastico del materiale [N/mm2], • ’sezione’: area della sezione trasversale [m2],
• ’momento_inerzia’: momento di inerzia della sezione trasversale [m4],
• ’densita’: densità del materiale [kg/m3],
• ’freq_sperimentali’: lista di frequenze sperimentali, con ’Null’ al posto dei dati mancanti [Hz]
Gli altri dati che servono sono le soluzioni al problema analitico associato al modello di catena scelto (vincoli simmetrici o asimmetrici, massa aggiunta,...), ovvero i contenuti delle tabelle create nella fase precedente. Il vantaggio di questo programma è proprio l’uso delle tabelle, che pur richidendo molto tempo per essere create, rendono poi il procedimento di ottimizzazione molto più rapido. Qui di seguito vengono importati i contenuti delle tabelle.
1 failozzo = h5py.File('dati_ottimizzazione.hdf5','r') 2 arr_el = failozzo['tabelle/molle_simmetriche'] 3 failozzo.close
8.3.2 Funzioni necessarie
Di seguito vengono definite le funzioni necessarie a risolvere il proble- ma inverso, ovvero ad ottenere una stima delle incognite (tiro e grado di vincolo) a partire dalle frequenze di vibrazione misurate. Per farlo, le funzioni prendono in ingresso i dati relativi alla trave, contenuti in una struttura a dizionario (che potrà eventualmente essere importata da un file tipo foglio di calcolo per analizzare batch di catene), e una delle tabelle generate nella fase precedente, secondo le ipotesi che vengono fatte sulla catena.
Funzioni accessorie di conversione
Queste funzioni servono a passare dai dati misurati al valore dei parametri adimensionali corrispondenti, e viceversa.
1 def fromBetaToFreq(lista_beta, datiTrave): 2 """
3 Restituisce una lista di frequenze da una lista di beta. 4 datiTrave è un dizionario contenente i dati della trave: 5 {'lunghezza', 'modulo_young', 'sezione',
6 'modulo_inerzia', 'densita', 'freq_sperimentali'} 7 frequenze_sperimentali è un array monodimensionale; 8 """
9 L = datiTrave['lunghezza'] 10 E = datiTrave['modulo_young'] 11 A = datiTrave['sezione']
12 J = datiTrave['momento_inerzia'] 13 rho = datiTrave['densita']
14 molt = np.sqrt(rho*A/(E*J))*L**2*2*np.pi 15 return lista_beta/molt
16
17 def fromFreqToBeta(lista_freq, datiTrave): 18 """
19 Restituisce una lista di beta da una lista di frequenze. 20 datiTrave è un dizionario contenente i dati della trave: 21 {'lunghezza', 'modulo_young', 'sezione',
22 'modulo_inerzia', 'densita', 'freq_sperimentali'} 23 frequenze_sperimentali è un array monodimensionale; 24 """
26 E = datiTrave['modulo_young'] 27 A = datiTrave['sezione']
28 J = datiTrave['momento_inerzia'] 29 rho = datiTrave['densita']
30 molt = np.sqrt(rho*A/(E*J))*L**2*2*np.pi 31 return lista_freq*molt
32
33 def fromAlphaToTiro(alpha, datiTrave): 34 """
35 Calcola il tiro corrispondente ad un dato valore di alpha. 36 datiTrave è un dizionario contenente i dati della trave: 37 {'lunghezza', 'modulo_young', 'sezione',
38 'modulo_inerzia', 'densita', 'freq_sperimentali'} 39 frequenze_sperimentali è un array monodimensionale; 40 """ 41 L = datiTrave['lunghezza'] 42 E = datiTrave['modulo_young'] 43 J = datiTrave['momento_inerzia'] 44 molt = 2.*(E*J)/L**2 45 return alpha*molt 46
47 def fromTiroToAlpha(tiro, datiTrave): 48 """
49 Calcola alpha corrispondente ad un dato valore del tiro. 50 datiTrave è un dizionario contenente i dati della trave: 51 {'lunghezza', 'modulo_young', 'sezione',
52 'modulo_inerzia', 'densita', 'freq_sperimentali'} 53 frequenze_sperimentali è un array monodimensionale; 54 """ 55 L = datiTrave['lunghezza'] 56 E = datiTrave['modulo_young'] 57 J = datiTrave['momento_inerzia'] 58 molt = L**2/(2*E*J) 59 return tiro*molt
Funzione di interpolazione nelle tabelle
Questa funzione serve per trovare la lista di valori di beta (e di conse- guenza le frequenze) corrispondenti ad un valore di alpha e ad uno di theta che non sono esplicitamente contemplati dalle tabelle. Lo fa attraverso un’interpolazione lineare. Non è in grado di estrapolare.
1 def cercaBeta(theta, alpha, tabella): 2 """
3 Restituisce la lista di valori di beta relativa ad un 4 valore di alpha e di theta, a partire dalla tabella
5 generata in precedenza. Per i valori intermedi tra quelli 6 considerati nella tabella, interpola linearmente.
7 NOTA: questa versione della funzione ha bisogno di una 8 tabella lineare in alpha e theta.
9 """ 10
11 t = theta 12 a = alpha 13
14 #Valori presenti in tabella tra i quali è compreso alpha 15 amin = int(np.floor(a))
16 amax = int(np.ceil(a)) 17
18 #Valori presenti in tabella tra i quali è compreso theta 19 tmin = int(np.floor(t))
20 tmax = int(np.ceil(t)) 21
22 #Estremi tra i quali viene fatta l'interpolazione 23 punti = np.array([[tmin, amin], [tmax, amin],
24 [tmax, amax], [tmin, amax]])
25
26 if (a == amax or a == amin) and (t == tmax or t == tmin): 27 return tabella[int(t), int(a), :]
28
29 listaBeta = [] #Lista che verrà popolata con i beta
30 #corrispondenti a TH ed a
31
32 #Un modo alla volta
33 for i in range(tabella.shape[2]):
34 #caso in cui theta è contemplato dalla tabella 35 if t == tmin or t == tmax:
36 bmax = tabella[int(t), amax, i] 37 bmin = tabella[int(t), amin, i]
38 b = bmin + (bmax - bmin)*(a - amin)/(amax - amin)
39 listaBeta.append(b)
40 #caso in cui alpha è contemplato dalla tabella 41 elif a == amin or a == amax:
42 bmax = tabella[tmax, int(a), i]
43 bmin = tabella[tmin, int(a), i]
44 b = bmin + (bmax - bmin)*(t - tmax)/(tmax - tmin)
45 listaBeta.append(b)
46 else:
47 valori = [tabella[punti[k,0], punti[k,1], i] 48 for k in range(4)]
49 interpolatore = LinearNDInterpolator(punti, valori) 50 listaBeta.append(float(interpolatore.__call__(t, a)))
51
52 return listaBeta
Funzione di errore
Viene qui definita una funzione che, valutata per una data catena reale ed una "ideale", restituisce un errore. Questo viene calcolato a partire dalle differenze tra le frequenze misurate e quelle calcolate, ed è necessario al processo di minimizzazione che troverà la catena "ideale" più simile possibile a quella reale. Il procedimento è diviso in due funzioni. Una si occupa di calcolre l’errore avendo già a disposizione due serie di frequenze da confrontare; la seconda usa la prima, ma si preoccupa anche di recuperare (per interpolazione dalla tabella appropriata) le frequenze teoriche.
1 def errore(freq_sperimentali, freq_teoriche): 2 """
3 Calcola la somma degli scarti quadratici tra frequenze 4 sperimentali e teoriche pesati sulla frequenza sperimentale 5 (frequenza maggiore -> minore peso). Se mancano alcuni 6 valori sperimentali si inseriscono stringhe 'Null' nel 7 vettore delle frequenze sperimentali, e questi modi vengono 8 saltati nel computo dell'errore.
9 """ 10 err = 0.0 11 i = 0
12 while i < len(freq_sperimentali): 13 if freq_sperimentali[i] == 'Null':
14 i = i+1
15 else:
16 err = err + np.sqrt(
17 (1-freq_teoriche[i]/float(freq_sperimentali[i]))**2)
18 i = i+1
19 return err 20
21 def funzioneObiettivo(x, datiTrave, tabella, tmin = 0., tmax = 1000.): 22 """
23 Restituisce l'errore calcolato per x=[theta, alpha] e 24 per una data serie di frequenze sperimentali.
25 datiTrave è un dizionario contenente i dati della trave: 26 {'lunghezza', 'modulo_young', 'sezione',
27 'modulo_inerzia', 'densita', 'freq_sperimentali'} 28 frequenze_sperimentali è un array monodimensionale; 29 tabella è lineare in theta e alpha.
30 """
32 for el in datiTrave['freq_sperimentali']: 33 freq_sperimentali.append(el) 34 35 t = x[0] 36 a = x[1] 37 38 if t > tmax or t < tmin or a > 500. or a < 0.: 39 return 100 40
41 lista_beta = cercaBeta(t, a, tabella)
42 freq_teoriche = fromBetaToFreq(lista_beta, datiTrave) 43 err = errore(freq_sperimentali, freq_teoriche)
44 return err
Funzioni di ottimizzazione (con l’uso di tabelle)
Qui finalmente si procede alla scrittura delle funzioni necessarie a fare l’ottimizzazione, ovvero a ricavare una stima di tiro e grado di vincolo. Dal momento che il procedimento di minimizzazione ha bisogno di ricevere in ingresso una prima stima del risultato (da cui partire), la prima funzione stimaIniziale scorre la tabella alla ricerca di un buon punto di inizio. La seconda funzione, ottimizzatore, richiama due diverse funzioni già implementate nella libreria scipy. La prima,
basinhopping, è un metodo di minimizzazione globale che a sua volta
richiede un algoritmo di minimizzazione locale (in questo caso Powell); la seconda, minimize, offre invece algoritmi di minimizzazione locale, e in questo caso è scelto di usare l’algoritmo Nelder-Mead (o del simplesso). La scelta tra l’uso di un ottimizzatore locale (più veloce ma accurato solo se non ci sono più minimi locali) e un ottimizzatore globale (più accurato ma più lento) è lasciata all’utente.
1 def stimaIniziale(datiTrave, tabella, tmin=0.): 2 """
3 Restituisce un ndarray [theta, alpha] con stime dei 4 parametri per far partire l'ottimizzazione.
5 datiTrave è un dizionario contenente i dati della trave: 6 {'lunghezza', 'modulo_young', 'sezione',
7 'modulo_inerzia', 'densita', 'freq_sperimentali'} 8 frequenze_sperimentali è un array monodimensionale; 9 tabella deve essere lineare in alpha e theta. 10 """
11
12 #Cerca il primo valore non nullo tra freq.sperimentali 13 trovato = False
14 i = 0
16 if datiTrave['freq_sperimentali'][i] == 'Null': 17 i = i+1 18 else: 19 start = i 20 trovato = True 21
22 freq_1 = float(datiTrave['freq_sperimentali'][start]) 23 beta = fromFreqToBeta(freq_1, datiTrave)
24
25 theta = 0
26 while (tabella[theta, 0, start] < beta and 27 theta < tabella.shape[0]-1):
28 theta = theta+1
29
30 alpha = 0
31 while (tabella[theta, alpha, start] < beta and 32 alpha < tabella.shape[1]-1):
33 alpha = alpha+1
34
35 return np.array([theta, alpha]) 36
37 def ottimizzatoreLineare(dati, tabella, minimizzatore='globale', 38 tmin = 0., tmax = 1000., verbose=True):
39 """
40 Funzione che restituisce un dizionario con alpha, theta ed 41 errore corrispondenti ai dati inseriti e ottenuti dalla 42 minimizzazione.
43 """
44 x0 = stimaIniziale(dati, tabella) 45
46 if minimizzatore == 'globale':
47 minimo = basinhopping(lambda x: funzioneObiettivo(x,
48 dati, tabella, tmin=tmin, tmax=tmax),
49 x0, stepsize=10,
50 minimizer_kwargs={'method':'Powell'})
51 elif minimizzatore == 'locale':
52 minimo = minimize(lambda x: funzioneObiettivo(x,
53 dati, tabella, tmin=tmin, tmax=tmax),
54 x0, method='Nelder-Mead')
55 else:
56 print("errore parametro minimizzatore") 57
58 theta = minimo['x'][0] 59 alpha = minimo['x'][1] 60 errore = minimo['fun']
62
63 if verbose == True:
64 print('x0 = ' + str(x0[0]) + ', ' + str(x0[1])) 65 print(minimo['message'])
66 print('f_errore = ' + str(errore)) 67 print("Tiro = "+str(tirokN)+" kN")
68 print("kvincolo = "+str(theta)+" * EJ/L\n") 69
70 return {'theta':theta, 'alpha':alpha, 'errore':errore}
Funzioni di ottimizzazione (senza l’uso di tabelle)
In alcuni casi, può essere necessario procedere all’ottimizzazione senza usare le tabelle. Di seguito alcune funzioni utili allo scopo. Scegliere di non usare le tabelle può sembrare sensato qualora non si voglia introdurre l’errore che necessariamente deriva dall’interpolazione. Pro- cedere in questo modo richiede tuttavia tempi molto maggiori nel calcolo della funzione di errore, tempi che possono diventare inge- stibili specialmente nel caso dell’algoritmo di ottimizzazione globale basinhopping, che richiede di valutare la funzione obiettivo un gran numero di volte.
Le seguenti due funzioni servono in caso di trave con vincoli simmetrici.
1 def funzioneObiettivoNOTAB(x, datiTrave): 2 """
3 Restituisce l'errore calcolato per x=[theta, alpha], e 4 per una data serie di frequenze sperimentali.
5 datiTrave è un dizionario contenente i dati della trave: 6 {'lunghezza', 'modulo_young', 'sezione',
7 'modulo_inerzia', 'densita', 'freq_sperimentali'} 8 frequenze_sperimentali è un array monodimensionale; 9 """
10 freq_sperimentali = []
11 for el in datiTrave['freq_sperimentali']: 12 freq_sperimentali.append(el) 13 14 theta = x[0] 15 alpha = x[1] 16 17 if alpha < 0.: 18 return 1000 19
20 lista_beta = listaZeri(lambda x: detEl(theta, alpha, x)) 21 freq_teoriche = fromBetaToFreq(lista_beta, datiTrave)
22 err = errore(freq_sperimentali, freq_teoriche) 23 return err
24
25 def ottimizzatoreNOTAB(dati, arr_el, minimizzatore='globale'): 26 """
27 Funzione che restituisce un dizionario con alpha 28 e theta corrispondenti ai dati inseriti e ottenuti 29 dalla minimizzazione. 30 """ 31 x0 = stimaIniziale(dati, arr_el) 32 print('x0 = ' + str(x0[0]) + ', ' + str(x0[1])) 33 34 if minimizzatore == 'globale':
35 minimo = basinhopping(lambda x: funzioneObiettivoNOTAB(x,
36 dati), x0, stepsize=10,
37 minimizer_kwargs={'method':'Powell'})
38 elif minimizzatore == 'locale':
39 minimo = minimize(lambda x: funzioneObiettivoNOTAB(x,
40 dati), x0, method='Nelder-Mead')
41 else:
42 print("errore parametro minimizzatore") 43
44 theta = minimo['x'][0] 45 alpha = minimo['x'][1]
46 tirokN = fromAlphaToTiro(alpha, dati)/1000. 47
48 print(minimo['message'])
49 print('f_errore = ' + str(minimo['fun'])) 50 print("Tiro = %s kN") %tirokN
51 print("kvincolo = %s * EJ") %theta 52
53 return [theta, alpha]
A seguire le stesse funzioni, ma per il caso di vincoli asimmetrici:
1 def funzioneObiettivoAsimNOTAB(x, datiTrave): 2 """
3 Restituisce l'errore calcolato per x=[theta, gamma, alpha], 4 e per una data serie di frequenze sperimentali.
5 datiTrave è un dizionario contenente i dati della trave: 6 {'lunghezza', 'modulo_young', 'sezione',
7 'modulo_inerzia', 'densita', 'freq_sperimentali'} 8 frequenze_sperimentali è un array monodimensionale; 9 """
10 freq_sperimentali = []
12 freq_sperimentali.append(el) 13 14 theta = x[0] 15 GA = x[1] 16 alpha = x[2] 17 18 if alpha < 0. or GA < 0. or GA > 1. or theta < 0.: 19 return 10000 20
21 lista_beta = listaZeri(lambda x: detAsim(theta, GA, alpha, x)) 22 freq_teoriche = fromBetaToFreq(lista_beta, datiTrave)
23 err = errore(freq_sperimentali, freq_teoriche) 24 return err
25
26 def ottimizzatoreAsimNOTAB(dati, arr_el, minimizzatore='globale'): 27 """
28 Funzione che restituisce un dizionario con alpha e theta
29 corrispondenti ai dati inseriti e ottenuti dalla minimizzazione. 30 """ 31 x0 = [stimaIniziale(dati, arr_el)[0], 1., 32 stimaIniziale(dati, arr_el)[1]] 33 print('x0 = ' + str(x0[0]) + ', ' + str(x0[1]) 34 + ', ' + str(x0[2])) 35 36 if minimizzatore == 'globale':
37 minimo = basinhopping(lambda x: funzioneObiettivoAsimNOTAB(x,
38 dati), x0, stepsize=10,
39 minimizer_kwargs={'method':'Powell'})
40 elif minimizzatore == 'locale':
41 minimo = minimize(lambda x: funzioneObiettivoAsimNOTAB(x,
42 dati), x0, method='Nelder-Mead')
43 else:
44 print("errore parametro minimizzatore") 45
46 theta = minimo['x'][0] 47 gamma = minimo['x'][1] 48 alpha = minimo['x'][2]
49 tirokN = fromAlphaToTiro(alpha, dati)/1000. 50
51 print(minimo['message'])
52 print('f_errore = ' + str(minimo['fun'])) 53 print("Tiro = "+str(tirokN)+" kN")
54 print("kvincolo = "+str(theta)+" * EJ") 55 print("gamma = "+str(gamma))
56
8.3.3 Rappresentazione delle funzioni di errore
In alcuni casi può risultare utile controllare la forma della funzione obiettivo per valutare qualitativamente la sua dipendenza dalle varia- bili incognite. È agevole farlo solamente finché il numero di incognite è uguale o inferiore a due, quindi al più per il caso di catene vincolate simmetricamente. Qui di seguito viene proposta una funzione che serve proprio per questo caso.
1 def disegnaFunzioneObiettivo(datiCatena, f=funzioneObiettivo, 2 tab=arr_el, alpharange=[0.,500.],
3 thetarange=[0.,1000.],
4 alphastep=1., thetastep=1.):
5 """
6 Funzione che produce un plot della funzione di errore *f*, 7 calcolata sulla catena i cui dati sono immagazzinati nel 8 dizionario *datiCatena*.
9 *datiCatena* = dizionario con i dati della catena secondo
10 lo standard;
11 *f* = funzione di errore da plottare;
12 *tab* = tabella con i dati relativi al modello di catena
13 scelto;
14 *alpharange* = lista con minimo e massimo valore di alpha; 15 *thetarange* = lista con minimo e massimo valore di theta; 16 *alphastep* = risoluzione lungo alpha;
17 *thetastep* = risoluzione lungo theta. 18 """
19 alpha = np.arange(alpharange[0], alpharange[1], alphastep) 20 theta = np.arange(thetarange[0], thetarange[1], thetastep) 21
22 #Calcolo dei valori e reshaping dell'array 23 res = [f([t, a], datiCatena, tab) for t in theta
24 for a in alpha]
25 resre = np.reshape(np.array(res), (len(alpha), len(theta))) 26
27 #Plot
28 fig = plt.figure(num=None, figsize=(12, 8), dpi=300, 29 facecolor='w', edgecolor='r') 30 TH, A = np.meshgrid(theta, alpha) 31 ax = fig.add_subplot(111, projection='3d') 32 33 Z = resre/0.5 34 #colors = cm.magma(Z)
35 #rcount, ccount, _ = colors.shape 36
38 ax.set_xlabel('theta') 39 ax.set_ylabel('alpha') 40 ax.set_zlabel('f. obiettivo') 41 ax.yaxis.pane.fill = False 42 ax.zaxis.pane.fill = False 43 ax.grid(False)
44 #ax.plot_wireframe(TH, A, resre, cstride=10,
45 rstride=0, cmap=colors)
46 ciccio = ax.plot_surface(TH, A, resre) 47 ciccio.set_facecolor((0,0,0,0))
48
49 #ax.view_init(azim=80)
8.3.4 Valutazione del tiro con θ vincolato
In alcuni casi, il procedimento ha mostrato la tendenza a fornire risul- tati di dubbia accuratezza. In particolare, analizzando serie di catene molto simili e presumibilmente installate nelle stesse condizioni, è capitato di ottenere valutazioni molto simili del tiro insieme a sti- me molto diverse del grado di vincolo. Questi risultati si ottengono specialmente utilizzando procedimenti di minimizzazione locale, e sono in buona parte spiegabili con la forma funzione obiettivo: una "valle", segnata da un gradiente molto elevato in direzione del tiro ed uno ordini di grandezza inferiore in direzione del grado di vincolo. Per permettere una sicurezza maggiore nella valutazione del tiro, è stato utile in alcuni casi ripetere la stessa minimizzazione vincolata, restringendo via via il range di variazione di θ. In questo modo, si ottengono stime successive della funzione di errore nel suo punto di minimo.
1 def possibilitaCatena(datiCatena, tabella, 2 minimizzatore='globale'):
3 """
4 Restituisce una lista di liste con i possibili risultati 5 per TH variabili. Ogni elemento è una lista
6 [tiro, theta, errore].
7 *datiCatena* = dizionario contenente i dati della 8 catena, secondo lo standard;
9 *tabella* = tabella con i dati relativi al modello di 10 catena scelto;
11 *minimizzatore* = locale o globale, parametro passato 12 alla funzione *ottimizzatore*.
13 """
14 possibilita = [ottimizzatore(datiCatena, tabella, 15 minimizzatore=minimizzatore, limInfLTH=ciccio)
16 for ciccio in np.arange(0., 900., 100.)]
17 res = [[fromAlphaToTiro(pippo['alpha'], datiCatena)/1000., 18 pippo['theta'], pippo['errore']]
19 for pippo in possibilita]
20
21 return res
8.4 fase 3: misurazioni acustiche di frequenza