POLITECNICO DI MILANO Corso di Studi in Ingegneria Matematica PROGETTO DI PROGRAMMAZIONE AVANZATA Professor Luca Formaggia ANNO ACCADEMICO 2006-2007 IMPLENTAZIONE C++ DI STRATEGIE PER L’ADATTAZIONE DI GRIGLIE BIDIMENSIONALI NON STRUTTURATE Marta D’Elia Matr. 679976 Matilde Dalla Rosa Matr. 680457 1 Poi nella notte s’ode un click sopra il Patch ha fatto crack s’è sparato il bel pinguino in frac Trio Lescano 2 Introduzione Questo lavoro si colloca nell’ambito del corso di Programmazione Avanzata per il Calcolo Scientifico e si pone l’obiettivo di implementare alcune strategie per l’adattazione della griglia di calcolo sulla base di una particolare stima dell’errore sulla soluzione agli Elementi Finiti ottenuta con la tecnica Super convergent Patch Recovery [1],[2],[3]. La risoluzione numerica tramite metodi agli Elementi Finiti di un problema a derivate parziali è sensibile alla qualità della discretizzazione spaziale del suo dominio. Accade infatti che tanto più la griglia è fitta, tanto più la soluzione è accurata (tale infittimento è noto sotto il nome di h-refinement). Ovviamente l’infittimento tout-court della griglia comporta l’innalzamento degli oneri computazionali, che tipicamente si preferirebbe evitare. A questo proposito sono stati proposti in letteratura dei metodi per cui è previsto non tanto l’aumento del numero di nodi, quanto un loro infittimento mirato laddove la soluzione sia particolarmente difficile da cogliere a causa, ad esempio, della presenza di forti gradienti. Da quest’idea nascono opportuni stimatori a posteriori dell’errore utili per ricavare informazioni di tipo spaziale che consentano di individuare zone nelle quali la soluzione calcolata richiede maggiore accuratezza. A partire da queste stime è possibile dedurre nuovi parametri ottimali (nella fattispecie la metrica ottimale) associati alla mesh di calcolo in corrispondenza dei quali la nuova griglia sia infittita nelle zone di interesse consentendo l’ottenimento di soluzioni migliori. iterazione K K=K+1 CALCOLO Uh SULLA MESH CORRENTE 1 ANALISI ERRORE CON STIMATORE A POSTERIORI 2 NO PROCEDURA DI OTTIMIZZAZIONE: METRICA OTTIMALE TOLLERANZA SODDISFATTA ? 3 PROCEDURA DI ADATTAZIONE DELLA MESH 4 SI END Fig. 1: Diagramma di flusso dell’intera procedura di adattazione Il presente lavoro parte dal presupposto di disporre di tali parametri ottimali e vuole proporre l’implementazione di alcune semplici strategie di adattazione (quali il movimento dei nodi e lo scambio delle diagonali dei quadrilateri formati da due triangoli adiacenti) per adattare la griglia coerentemente 1 Notazioni e formulazione del problema 3 λ 1,K TK r r1,K K ^ K r2,K 1 λ 2,K Fig. 2: Mappa affine TK con la fisica del problema in esame. Il processo di adattazione delineato è illustrato in modo chiaro e sintetico in Figura 1. Nel Paragrafo 1 introduciamo le principali notazioni utilizzate, l’ambientazione matematica del problema, la sua formulazione forte e quella variazionale ed i parametri determinanti la griglia di calcolo: si tratta di una triangolazione non strutturata, dunque ad ogni elemento si associa la metrica che ne definisce, tramite una matrice, misura ed orientazione. Nel Paragrafo 2 richiamiamo brevemente i passi 2 e 3 del processo adattivo di Figura 1: riportiamo i risultati relativi alla stima dell’errore e la procedura di ottimizzazione. Nel Paragrafo 3 illustriamo la parte centrale del lavoro: vengono delineate le caratteristiche di una triangolazione, le due tecniche di adattazione usate e gli algoritmi che ne consentono l’implementazione. Il Paragrafo 4 descrive invece nel dettaglio la struttura del codice C++ utilizzato: vengono descritte le classi implementate, i relativi metodi e la struttura del main. Illustriamo anche brevemente l’interazione con il software FreeFem++ che implementa la procedura di ottimizzazione. Nel Paragrafo 5 presentiamo i casi test trattati ed i relativi risultati ottenuti a seguito del processo di adattazione. Il Paragrafo 6 riporta le conclusioni sul lavoro svolto e propone eventuali sviluppi al codice: le strategie di adattazione presentate non sono le uniche implementabili, ma possono essere integrate con altri eventuali approcci. La struttura del codice presentato si mostra predisposta ad ogni eventuale modifica o estensione. 1 Notazioni e formulazione del problema Descriviamo in questo paragrafo i parametri associati ad una griglia, nello specifico introduciamo le notazioni associate alla metrica che saranno utilizzate nel seguito; inoltre specifichiamo l’ambientazione matematica del problema differenziale che vogliamo risolvere e sulla cui soluzione vogliamo sperimentare le tecniche di adattazione. Sia Ω un dominio poligonale di R2 con frontiera Lipschitz-continua ∂Ω; sia Th ∀ h : 0 < h ≤ 1, una famiglia di triangolazioni conformi del dominio Ω, ciascuna delle quali è costituita da triangoli K di b → K la mappa invertibile affine definita dal diametro hK ≤ h, dove h = max hK . Sia inoltre TK : K K∈Th b (equilatero di lato triangolo di riferimento K Figura 2): √ 3 2 ) al generico K, tale che (secondo quanto mostrato in 1 Notazioni e formulazione del problema 4 b b b +b x = TK (b x) = M K x tK ∀b x ∈ K, tK ∈ R2 (1) MK = BK ZK (2) Nel corso della nostra trattazione faremo riferimento alla matrice MK come metrica associata ad ogni triangolo; per mettere in evidenza le caratteristiche anisotrope della griglia che vogliamo costruire, si consideri la seguente decomposizione polare della matrice Jacobiana della trasformazione TK : con ZK ortogonale e BK matrice simmetrica e definita positiva, tale che ammetta la seguente decomposizione spettrale: T BK = RK Λ K RK (3) T = [r con ΛK = diag(λ1,K , λ2,K ), matrice degli autovalori di BK e con RK 1,K , r2,k ], matrice degli λ1,K autovettori di BK . Introduciamo ∀ K ∈ Th il rapporto : sK = λ2,K , con λ1,K < λ2,K , esso sarà detto fattore di stretching. Per il triangolo di riferimento vale invece sKb = 1. Introduciamo quindi il generico problema che vogliamo trattare e sulla base del quale vogliamo implementare la procedura adattiva. Sia Ω tale che ΓD e ΓN siano due sue opportune partizioni disgiunte, con ΓD 6= ∅ e ∂Ω = ΓD ∪ ΓN ; esse sono rispettivamente la frontiera con dato di Dirichlet e di Neumann. Consideriamo il seguente problema modello di diffusione-trasporto-reazione: Problema 1 (Formulazione forte). Trovare u : Ω → R tale che: −∇ · (a ∇u) + (b · ∇)u + cu in Ω; u = 0, su ΓD ; ∂u a ∂nL = g, su ΓN . (4) Al problema 1 associamo la seguente formulazione variazionale, scelto V lo spazio HΓ1D (Ω): Problema 2 (Formulazione variazionale). Trovare u ∈ V ≡ HΓ1D (Ω) tale che: Z Z Z (a∇u · ∇v + (b · ∇)uv + cuv)dx = f v dx + gv ds ∀ v ∈ V Ω Ω (5) ΓN con: f ∈ L2 (Ω) g ∈ L2 (∂Ω) c = c(x) > 0 q.o. x ∈ Ω b = (b1 , b2 )T e ∇ · b = 0 aij = aij (x) ∈ L∞ (Ω) t.c. ai,j ≥ 0 2 X ∂u ∂u aij =− ni ∂nL ∂xj i,j=1 n = (n1 , n2 )T (6) 2 Stimatore a posteriori dell’errore e procedura di adattazione 5 Queste ipotesi sui dati che garantiscono l’esistenza e l’unicità della soluzione u della formulazione variazionale. Collochiamoci ora in uno spazio finito dimensionale Vh ⊂ V costituito dalle funzioni lineari a tratti; proiettiamo il problema 2 in questo sottospazio ed applichiamo la tecnica StreamlineDiffusion per la sua stabilizzazione (rimandiamo a [7] per maggiori dettagli) mediante τk , coefficiente di stabilizzazione. Otteniamo il seguente Problema 3 (Formulazione discreta). Trovare uh ∈ Vh tale che Aτ (uh , vh ) = Lτ (vh ) ∀vh ∈ Vh (7) dove le forme Aτ : V × V → R e Lτ : V → R sono definite nel modo seguente: Aτ (uh , vh ) = Z Ω a∇uh · ∇vh dx + Z Ω ((b · ∇)uh + cuh )vh dx + Lτ (vh ) = Z Ω 2 f vh dx + Z X Z K∈Th K τk [(b · ∇)uh ][(b · ∇)vh ] dx ΓN gvh ds + X K (8) τK f ((b · ∇)vh ) dx Stimatore a posteriori dell’errore e procedura di adattazione Richiamiamo in questo paragrafo i risultati principali relativi alle stime dell’errore ed alla procedura di ottimizzazione della griglia; per maggiori dettagli rimandiamo a [7]. La seguente disuguaglianza riporta una stima dell’errore di discretizzazione in norma dell’energia dipendente dalla metrica associata alla mesh, vale dunque la seguente: Proposizione 1 (Stima a posteriori anisotropa per l’errore). Sia eh = u−uh l’errore di discretizzazione. Allora: X τK 1 keh k2 ≤ C krK (uh )kL2 (K) 1 + kbkL∞ (K) + kRK (uh )kL2 (∂K) × 1 λ2,K 2 2λ2,K K∈Th (9) X 1 λi,K 2 (rTi,K GK (eh )ri,K )} 2 i∈{1,2} dove keh k2 = A0 (eh , eh ), essendo A0 la forma bilineare non stabilizzata. Specifichiamo di seguito i termini che compaiono nella stima: rK (uh ) = f + ∇ · (a∇uh ) − b · ∇uh − cuh 0, E ∈ ∂K ∩ εD ; ∂uh −2 g − ∂nL,K , E ∈ ∂K ∩ ΓN ∩ εK ; RK (uh ) = h i − ∂uh E ∈ εK . ∂nL,K E , R ∂v ∂v R ∂v 2 ( ∂x1 )( ∂x2 )dx ( ∂x1 ) dx T T X R ∂v 2 GK (v) = R ∂v ∂v )( )dx ) dx ( ( T ∂x1 ∂x2 T ∂x2 T ∈∆K (10) 2 Stimatore a posteriori dell’errore e procedura di adattazione 6 Tale stima, come notiamo dalla (10) dipende dal gradiente della soluzione esatta; mediante la tecnica Recovery Based di ricostruzione del gradiente possiamo esprimere tale matrice GK in funzione della soluzione calcolata uh (per maggiori approfondimenti si veda [4]). A seguito dell’uso di questa tecnica possiamo dunque ritenere valida la seguente: Proposizione 2 (Stima a posteriori anisotropa Z-Z). Sia uh la soluzione del problema discreto. Allora: " # 1 2 X 1 1 h 2 2 λ1,K keh k := |A(eh , eh )| ≤ C λ2,K ||ρK (uh )||L2 (K) + k ||RK (uh )||L2 (∂K) 2 K∈Th (11) " #1 2 1 T × sK (rT1,K GK (e∗h )r1,K ) + (r GK (e∗h )r1,K ) sK 2,K dove [GK (e∗h )]ij + X Z T ∈∆K T (GZZ i (uh ) − ∂uh ∂uh )(GZZ )dx j (uh ) − ∂xi ∂xj ZZ T GZZ (uh ) = (GZZ 1 (uh ), G2 (uh )) 1 X |T |∇uh T GZZ (uh ) = |∆i | (12) T ∈∆i Alla luce della proposizione appena enunciata possiamo definire il seguente stimatore: Definizione 1. Lo stimatore anisotropo a posteriori per la norma dell’energia dell’errore di discretizzazione è dato dalla quantità: X 1 2 2 η := ηK (13) K∈Th 1 con ηK := (αK ρK (uh )ωK (e∗h )) 2 indicatore dell’errore per ogni elemento dove: αK = λ1,K λ2,K , ρ(uh ) definito come sopra e 1 τK kRK (uh )k kbk) + 1 λ2,K 2 2 λ2,K " #1 2 1 wK (e∗h ) = sK (rT1,K GK (eh )r1,K ) + (rT2,K GK (eh )r1,K ) sK ρK (uh ) + krK (uh )k(1 + (14) La tecnica di adattazione a questo punto può essere basata su una procedura iterativa per la costruzione di una successione di soluzioni e di griglie che soddisfano una condizione di ottimalità determinata mediante l’uso predittivo dello stimatore η. Supponiamo dunque, all’n-esimo passo della procedura, di essere in possesso di una soluzione unh calcolata sulla mesh Thn , siamo dunque in grado di calcolare η n sulla base di unh e di MTn n . Cerchiamo h dunque una metrica MTn+1 come funzione definita sulla mesh Thn sulla base della quale costruire Thn+1 . n h A partire dai risultati dedotti in [2] la metrica ottimale ad ogni passo si ottiene dalla risoluzione del seguente problema di minimo: 3 Le tecniche di adattazione 7 Determinare sK ed r1,K tali che 1 t (r G̃K (e∗h )r2,K ) sia minimo, con: sK 2,K ∈ R, ||r1,K || = ||r2,K || = 1, r1,K · r2,K = 0. sK (rt1,K G̃K (e∗h )r1,K ) + sK ≥ 1, r1,K , r2,K (15) La soluzione di tale problema è data dal seguente: Teorema 1. La soluzione (s̃K , r̃1,K ) del problema di minimizzazione è tale che r̃1,K è parallelo all’autovettore associato al minimo autovalore di G̃K (e∗h ) mentre !1 max(eig(G̃K (e∗h ))) 2 (16) s˜k = min(eig(G̃K (e∗h ))) con eig(G̃K (e∗h )) l’insieme di autovalori della matrice G̃K (e∗h ). Si tratta ora di esprimere gli autovalori associati alla nuova metrica per ogni triangolo: chiamiamo in causa un vincolo sull’accuratezza della soluzione e imponiamo ηK = τ ∀K ∈ Th con τ tolleranza fissata con lo scopo di uniformare l’errore su tutta la griglia. Gli autovalori saranno allora le soluzione del sistema seguente: λ̃1,K = s̃K = q λ̃2,K " #1 3 4 τ =p λ̃ λ̃ = σ 1,K 2,K |K̂|3 ρ̃2 (s̃K σ2,K + s̃1,K ) K 1 2 1 2 da cui possiamo dedurre che λ̃1,K = (pq) e λ̃2,K = ( pq ) ; ciò completa il passo n della procedura iterativa: possiamo quindi costruire Thn+1 e calcolare su essa una nuova soluzione. 3 Le tecniche di adattazione Nel paragrafo precedente è stata descritta la procedura per l’ottenimento della metrica ottimale, ma ancora non è stato spiegato come si debba agire sulla griglia per poterla trasformare coerentemente con i nuovi parametri. In letteratura sono presenti alcune strategie base per realizzare l’adattazione, che riassumiamo di seguito in tre principali categorie: Raffinamento/Deraffinamento della griglia Si tratta di aggiungere/togliere nodi della griglia in regioni selezionate sulla base delle informazioni acquisite dalla metrica, per poi qui ricostruire la mesh. Questa strategia presenta costi computazionali onerosi. Movimento dei nodi Si tratta di spostare i nodi verso le zone in cui la soluzione presenta elevati gradienti, senza però modificare la topologia della griglia. Questa strategia può essere globale, nel senso che tutti i nodi vengono spostati nello stesso momento, o locale, cioè viene preso in considerazione un nodo alla volta e lo spostamento è vincolato alla regione individuata dagli elementi che convergono nel nodo stesso. In entrambi i casi questa procedura è vantaggiosa in quanto a costi computazionali, che restano costanti, ma difficilmente produce griglie soddisfacenti se applicata da sola, per il vincolo sulla topologia. Sottolineiamo il fatto che applicando la procedura globale potrebbero insorgere problemi di inconsistenza. 3 Le tecniche di adattazione 8 P Pnew (a) Movimento locale dei nodi P2 P2 P3 P 3 P4 P4 P1 P1 (b) Scambio di lati Fig. 3: Operazioni di adattazione Remeshing globale/locale Consiste nel generare una mesh completamente nuova (globale) oppure di eliminare gli elementi della zona da adattare e generare una parte nuova di mesh (locale). In questo lavoro abbiamo scelto di concentrarci sullo sviluppo di una procedura di adattazione che preveda il solo movimento della griglia, che si avvale di due mosse di base: • Movimento locale dei nodi • Scambio delle diagonali di quadrilateri formati da due triangoli con un lato in comune La prima già introdotta in modo generale in precedenza prevede allora di considerare a turno ciascun nodo e di farlo muovere all’interno del suo patch. La seconda invece considera ciascun lato e i due triangoli che vi insistono, valutando l’opportunità di scambiare tale lato con quello che unisce i due vertici non comuni ai triangoli. In Figura 3 troviamo la rappresentazione grafica l’effetto delle due metodologie considerate. Nei prossimi paragrafi introdurremo gli elementi principali per la descrizione della triangolazione ed entreremo nel dettaglio degli algoritmi utilizzati. 3.1 Gli elementi della Triangolazione In questa sezione introduciamo le informazioni necessarie alla memorizzazione della griglia, in questo modo renderemo più chiare le notazioni utilizzate nella descrizione delle procedure di adattazione e degli algoritmi utilizzati. 3 Le tecniche di adattazione 9 10 3 4 1 1 3 9 7 9 8 4 8 6 5 2 7 2 6 5 Fig. 4: Triangolazione con orientazione intrinseca ed interna ai triangoli Una triangolazione è individuata da tre insiemi: quello dei nodi P + {Pi : i = 1, ...NP }, formato dai vertici di ogni triangolo; quello dei lati E + {Ei,j : Pi , Pj ∈ P}, a cui appartengono le coppie di nodi che formano i lati dei triangoli; quello dei triangoli T + {Ti : i = 1, ...NT } con NT numero di triangoli della mesh. Ogni lato può appartenere a due soli triangoli detti ad esso adiacenti, fatta eccezione per i lati di bordo. Introduciamo il concetto di orientazione, fondamentale nella memorizzazione della mesh. Associamo ad ogni elemento dei tre insiemi introdotti una numerazione globale che rimmarrà tale durante tutto il processo di adattazione. Ogni punto sarà identificato da una identità e dalle coordinate cartesiane x e y; allo stesso modo il lato sarà caratterizzato dall’identità e dalla coppia di punti che lo costituisce secondo la convenzione: Ei,j = (i, j) : i < j; (17) ad ogni lato sarà dunque associata un’orientazione intrinseca. Ogni triangolo sarà invece identificato dalla sua identità e da tre indici corrispondenti alle identità dei tre vertici nella numerazione globale: Ti,j,k : Pi , Pj , Pk ∈ P : i < j, i < k; (18) oltre alla condizione sugli indici indicata nella (18) c’è una convenzione sull’orientazione: i punti associati alla terna di indici devono essere tali da trovarsi orientati in senso antiorario nella griglia. Tale ordine induce un’orientazione interna in senso antiorario dei lati del triangolo. Ricordiamo però che ad ogni lato sono, in generale, associati due elementi; solo in corrispondenza di uno di essi l’orientazione interna risulta coerente con quella intrinseca del lato stesso. Associamo dunque ad ogni triangolo i tre lati a partire da quello opposto al primo vertice: ±(j, k); ±(k, i); ±(i, j) dove il segno indica la coerenza tra l’orientazione interna al triangolo e quella intrinseca del lato. Riportiamo in Figura 4 un esempio di triangolazione con orientazione intrinseca ed interna al triangolo. 3 Le tecniche di adattazione 10 nj Fj gj Kj P Si i E1 i E2 Fig. 5: Patch di un nodo con notazioni che fanno riferimento agli algoritmi implementati Oltre alla memorizzazione degli insiemi appena citati necessitiamo di due informazioni aggiuntive che memorizziamo in due ulteriori strutture. Associato ad ogni lato è fondamentale conoscere le identità ad esso adiacenti, chiameremo la struttura che conserva quest’informazione SideToElement. Per convenzione sarà indicato con 0 l’elemento a sinistra del lato e con 1 quello a destra (coerentemente con l’orientazione intrinseca del lato); qualora il lato fosse di bordo l’elemento mancante verrà indicato con −1. Da ultimo introduciamo la struttura dati che memorizza le informazioni relative al patch di un punto, faremo riferimento ad essa come struttura PointToSides. Per ogni punto della griglia memorizziamo il numero di lati incidenti in esso ed una lista del lati stessi in ordine antiorario. 3.2 Movimento dei nodi Molte delle procedure di movimento dei nodi che si trovano in letteratura sono basate sul cosiddetto Laplacian smoothing, che consiste nel considerare ad uno ad uno i nodi interni e nello spostarli nel baricentro del relativo patch. Tale nuovo punto può essere calcolato in più modi; nel seguito si è scelto di identificarlo con il centro di gravità del patch stesso, mediante la formula: R P x dΩ Gd = R P (19) PP dΩ Applicazioni ripetute di questa procedura hanno un effetto regolarizzante e possono essere applicate in modo iterativo, per ottenere una tecnica di ottimizzazione globale. Da sottolineare, in particolare, è la semplicità ed il costo computazionale contenuto. Per quel che riguarda la consistenza della griglia, ovvero la proprietà per cui i triangoli non hanno area negativa, possiamo dire che essa è garantita nel caso di patch convesso e può essere garantita anche nel caso di patch concavo, purchè si vada a spostare il nodo in modo opportuno, non più all’interno del patch ma in una sua sottoregione opportunamente individuata. Prima di delineare l’algoritmo di spostamento dei nodi, descriviamo però brevemente la tecnica utilizzata per discriminare se un patch è convesso oppure concavo. Dato il patch PP centrato nel punto P di coordinate (xP , yP ) in Figura 5, abbiamo a disposizione sicuramente tutte le coordinate dei vertici che 3 Le tecniche di adattazione 11 A A B B F F C C D D E E Fig. 6: Controllo di concavità di un patch lo individuano (xi , yi ) ∀ i : Pi ∈ ∂PP . L’idea è allora quella di fissare a turno due vertici e calcolare la relativa retta su cui essi giacciono, in modo da poter controllare se i restanti vertici giacciono nel medesimo semipiano rispetto al centro P . Questa proprietà vale solo nel caso in cui il patch sia convesso, mentre nel caso di patch concavo essa cade, come si può vedere in Figura 6 , in cui il vertice B non giace sullo stesso semipiano di P rispetto alla retta passante per i nodi A e D. L’algoritmo appena descritto in modo qualitativo può essere schematizzato come segue: dato P di centro P (xP , yP ) 1. Calcolare ∀ i , j coppia di vertici consecutivi: ∆x = xj − xi ∆y = yj − yi q = yj ∆x − xj ∆x valoreP = xP ∆y − yP ∆x + q; 2. Assegnare il semipiano a cui appartiene il centro: 1, se valoreP > 0 sp = 0, altrimenti 3. ∀ k 6= i, j vertice del patch assegnare: valorek = xk ∆y − yk ∆x + q; 1, se valorek > 0 sp = 0, altrimenti 4. Se sP 6= sk allora concavo. Un algoritmo analogo, ovvero basato sullo stesso principio di costruzione delle rette e confronto rispetto all’appartenenza ad un certo semipiano è stato pensato per stabilire se un generico punto appartiene o meno ad un triangolo. In questo caso si calcolano a turno i coefficienti dei semipiani individuati dai 3 Le tecniche di adattazione 12 C C P B B A A P (a) Il punto P che appartiene al triangolo (b) Il punto P non appartiene al triangolo Fig. 7: Appartenenza ad un triangolo lati del triangolo e si procede a confrontare che il punto in esame giaccia sul medesimo semipiano del vertice escluso del triangolo. Si veda come esempio la Figura 7. A questo punto passiamo all’algoritmo per lo spostamento dei nodi. La formula (19) diventa: Gd = xP + X 1 wKP (AE1,K + AE2,K )(xK − xP ) 3AP KinPP X 1 = xP + zKP (xK − xP ) 3AP (20) KinPP dove AE1,K e AE2,K sono le aree dei triangoli consecutivi E1,K ed E2,K separati dal lato K, AP è l’area del patch, mentre: P zKP = 1, in quanto combinazione convessa KinPP √ (21) (xK −xP )T MKP (xK −xP ) wKP = α con M metrica costante sul lato in questione KP ||xK −xP || Il parametro α che compare nella definizione di wKP è il fattore di scala che permette di ottenere la combinazione convessa e pertanto si ottiene imponendo la prima delle due equazioni della (21): α= P K∈PP √ 3APP (xK −xP )T MKP (xK −xP ) ||xK −xP || Considerato il patch PP centrato in P , controlliamo che il patch sia convesso. Nel caso lo sia, procediamo calcolando le nuove coordinate del centro e aggiorniamo le strutture dati. Nel caso non sia convesso, vogliamo comunque procedere allo spostamento del centro, considerando una sottoregione del patch, come in Figura 6. Si tratta quindi di determinare le combinazioni convesse dei nodi che individuano i vertici della sottoregione. L’algoritmo che si utilizza a tale scopo è il seguente: 1. Porre ∀ K triangolo del patch PP : 2. ∀ Fj ∈ FP lato che costituisce la frontiera del patch, calcolare: 3 Le tecniche di adattazione 13 • aj = (gj − xP · nj ) • ∀ K ∈ PP calcolare τ = (xK −xP )nj a2j e se τ < 1 porre τ = 1 e tK = min(tK , τ1 ) 3. Calcolare le coordinate dell’intersezione come: x̃ = xK − tK (xK − xP ) ∀ K In questo lavoro abbiamo anche esteso la possibilità di movimento per i nodi di bordo, ad esclusione ovviamente di quelli fissi, cioè dei nodi d’angolo. La nuova posizione del nodo in questo caso è vincolata a rimanere sul lato di bordo e non si può spostare all’interno del patch. 3.3 Lo Scambio delle Diagonali Descriviamo in questo paragrafo la tecnica di scambio delle diagonali dei quadrilateri; si tratta di una procedura che consente di cambiare la topologia della griglia senza modificare il numero e la posizione dei punti. L’apporto principale di questa tecnica è quello di migliorare la qualità della triangolazione mantenedo la connettività di ogni nodo vicina a quella ottimale (dove per connettività intendiamo il numero di lati incidenti in un dato nodo; quella ottimale è pari a 6 per un nodo interno, corrisponde cioè al numero di triangoli equilateri con un vertice in comune). Ci sono diverse strategie di implementazione di questa tecnica, di seguito riportiamo quella da noi utilizzata. La Figura 3 riporta l’esito di un’operazione di scambio per un lato scambiabile; due sono le condizioni che rendono tale un lato (che comportano quindi una nuova configurazione ancora ammissibile): • non è un lato di bordo, causa il vincolo dato dal dominio; • il quadrilatero formato dai due triangoli adiacenti è strettamente convesso in quanto vogliamo mantenere la griglia consistente. Introduciamo ora delle quantità che saranno utili nel seguito per definire i criteri di swapping: • De(P ): difetto di un punto, differenza tra la connettività attuale e quella ottimale De(P ) + Nconn (P ) − Nid (P ) (22) dove Nid (P ) = 6 con lato interno e Nid (P ) = 3 con lato di bordo non fisso. • Gain(S): guadagno associato ad un lato: Gain(S) + De(P1 ) + De(P2 ) − De(P3 ) − De(P4 ) (23) con Pi i = 1, ...4 definiti come in Figura 3. • Qual(E): indice di qualità di un elemento: det(M )A(E) Qual(E) + s 3 P ltj M lj j=1 (24) 4 Le strutture dati 14 dove M è la metrica costante sull’elemento corrente E, definita come media aritmetica della metrica sui nodi del triangolo stesso; lj è il vettore della differenza delle coordinate dei vertici del triangolo. Possiamo descrivere ora un algoritmo di scambio basato sulla connettività e la qualità degli elementi come segue: 1. considerare ogni lato scambiabile ed i suoi elementi adiacenti, siano essi E1 ed E2 ; scambiare il lato se vale almeno una delle seguenti condizioni: (a) Gain(S) > 2 (b) Gain(S) = 2 ∧ min{Qual(E1 ), Qual(E2 )} < min{Qual(E1∗ ), Qual(E2∗ )} dove con E1∗ ed E2∗ indichiamo gli elementi che si otterrebbero in seguito all’operazione di Diagonal Swapping; 2. continuare fino a quando nessun lato soddisfa tali condizioni o fino a che non viene raggiunto un massimo numero di iterazioni fissato. Osservazione 1. La procedura per il controllo di convessità del quadrilatero è basata sul medesimo algoritmo per il controllo di convessità di un patch (descritto nel paragrafo precedente). La differenza fondamentale sta nella scelta del punto di riferimento scelto per il controllo del segno. 4 Le strutture dati Veniamo ora alla scelta delle classi e delle strutture dati che consentono di memorizzare nel modo più efficiente tutte le informazioni di cui necessitiamo. Iniziamo con l’illustrare le classi che identificano i principali elementi della mesh: nodi, lati e triangoli. 4.1 La classe Point Per la memorizzazione dei nodi di griglia scegliamo di creare una classe che eredita da un tipo predefinito appartenete al pacchetto UBLAS, che quindi introduciamo: namespace UBLAS = boost::numeric::ublas La classe da cui Point eredita è quella dei vettori con capacità predefinita: class Point : public UBLAS::bounded_vector<Real, 2> Tale scelta nasce dalla necessità di memorizzare un numero noto di informazioni: le coordinate cartesiane x e y del nodo in questione. Aggiungiamo come attributi privati (di tipo int) l’identità del punto nella numerazione globale ed il label che identifica il punto in base alla sua appartenenza o meno al bordo della mesh, in tal caso specifica anche a quale sottoinsieme della frontiera. L’accesso alle coordinate del punto avviene attraverso l’operatore [ ], cosı̀ che associate al generico punto P , che identifichiamo con l’oggetto Point P, abbiamo: (xP , yP ) = (P [0], P [1]) 4 Le strutture dati 4.2 15 La classe Edge Per la memorizzazione di un lato scegliamo di implementare una classe senza legami di ereditarietà con classi predefinite o altre classi introdotte dal nostro codice. I suoi attributi sono i seguenti: private: std::vector<p2p> M_points; int Edge_ID; int Edge_label; I due punti costituenti il lato sono memorizzati in un std::vector i cui elementi sono di tipo p2p, definito come typedef Point* p2p; la loro memorizzazione avviene con un ordine coerente con l’orientazione intrinseca del lato. Gli ulteriori attributi privati (tutti di tipo int) memorizzano rispettivamente l’identità del lato e l’apparteneza o meno al bordo della griglia. Osservazione 2. La scelta di usare i puntatori ad oggetti Point nasce dalla necessità di non appesantire l’occupazione di memoria: evitare la creazione di copie di oggetti già esistenti rende il codice più articolato, ma comporta un grande risparmio di spazio; non solo: in questo modo i nodi costituenti la griglia vengono istanziati una sola volta e saranno memorizzati come tali in un’unica struttura dati, tutte le strutture o classi che richiameranno tali punti faranno riferimento, tramite puntatori, ad oggetti Point già memorizzati. Questo principio adottato nella classe Edge risulterà comodo e valido anche nel seguito nelle strutture che faranno riferimento ai nodi della griglia. Dal momento che il nostro scopo è infatti quello di agire sulla mesh di calcolo modificandola, diventa comodo poter agire solo sugli oggetti effettivamente istanziati ed avere gratuitamente l’aggiornamento di tutte le strutture che ad essi fanno riferimento. Tutti gli attributi citati sono membri privati della classe che dispone dunque di opportuni metodi di accesso agli stessi; in aggiunta ad essi abbiamo il metodo Edge::measure() atto a restituire, mediante la funzione norm_2 predefinita e presente nella libreria <cmath>, la lunghezza del lato. 4.3 La classe Our List Questa classe è creata a supporto della struttura dati PointToSides illustrata nel paragrafo precedente; essa eredita pubblicamente da un tipo della Standard Template Library: class Our_List : public std::list<Edge*> Si tratta dunque di una lista di puntatori ad Edge; in quanto pensata appositamente per la memorizzazione della struttura dati citata, essa viene corredata degli attributi (di tipo int) che memorizzano l’identità del punto del quale vogliamo memorizzare la lista dei lati incidenti, il numero di tali lati (detto header del punto) ed il numero di elementi del patch associato al nodo in questione. La scelta di una classe che erediti da std::list appare abbastanza naturale e comoda, gli elementi sono dei puntatori, ordinati in senso antiorario, ai lati incidenti nel nodo. Gli algoritmi che utilizziamo richiedono un facile scorrimento degli elementi della lista; inoltre nell’ambito della tecnica di Diagonal Swapping vengono eliminati alcuni lati ed altri ne vengono creati, l’uso di una lista rende più semplice quest’operazione che risulterebbe decisamente più lunga ed articolata mediante l’uso di un diverso contenitore quale lo std::vector. La lista permette di agire facilmente al suo interno nell’eliminazione di un elemento in quanto agisce solo sui puntatori ad elementi successivi senza effettivi spostamenti di informazioni nella memoria. 4 Le strutture dati 4.4 16 La classe Shape Introduciamo ora una classe astratta, e quindi non istanziabile, utile per trattare forme geometriche racchiuse da lati e da punti i cui attributi saranno dunque costituiti da contenitori di Point ed Edge. Nello specifico gli attributi privati sono tre interi che individuano numero di lati, il numero di punti e l’identità dell’oggetto; sono presenti inoltre due std::vector i cui elementi sono puntatori ai nodi vertici dell’oggetto e ai lati della griglia. std::vector<p2p> M_points; std::vector<Edge> M_edges; Dove p2p è il tipo già definito in Edge. I metodi associati alla classe sono tutti gli accessors agli attributi privati ed il metodo Shape::measure(), membro virtual che verrà ridefinito coerentemente nelle classi figlie; esse sono Triangle, centrale nella memorizzazione della mesh, e Patch. Prima di illustrare le classi figlie ricordiamo l’utilità dell’aver creato la classe astratta: essa consente di poter allargare la validità del codice anche a mesh formate, ad esempio, da quadrilateri. Procediamo dunque con l’illustrare le classi che ereditano da quella corrente: • Triangle: questa classe indivudua un elemento della mesh di calcolo in tutte le sue peculiarità. In aggiunta agli attributi privati di Shape abbiamo due int che fanno riferimento al all’appartenennza o meno al bordo della mesh e all’orientazione del primo lato associato al triangolo. Conformemente a quanto spiegato nella sezione precedente, l’orientazione interna al triangolo dei lati della mesh non va di pari passo con quella intrinseca; quello che si può notare è che il secondo e l’ultimo lato hanno sempre rispettivamente segno discorde e concorde con l’orientazione intrinseca (e questo deriva dalla memorizzazione dei punti) mentre il primo lato ha un’orientazione che dipende dalla numerazione globale del secondo e terzo nodo del triangolo; scegliamo dunque di memorizzare solo tale informazione. Tra i metodi abbiamo l’overloading del metodo Triangle::measure() che restituisce l’area del triangolo mediante la formula di Schoelace che riportiamo di seguito; detti (xi ; yi )i=1...3 i vertici in coordinate cartesiane, abbiamo 1 Area = 2 3 X i=1 xi yi+1 − 3 X i=1 xi+1 yi ! (25) con la convenzione che (x4 ; y4 ) = (x1 ; y1 ). Un altro metodo particolarmente utile è quello che, utilizzando l’algoritmo già citato, controlla l’appartenenza di un generico punto al triangolo in questione (Triangle::is_in_Triangle(Point P)). • Patch: questa classe è fondamentale nella procedura di adattazione basata sullo spostamento dei nodi. In questo caso i nodi ed i lati della Shape in questione sono costituiti dai vertici dei lati del patch associato ad un punto e dai corrispondenti lati che li uniscono. In aggiunta ai membri privati aggiungiamo un puntatore a Point, esso punta all’oggetto che si identifica con il centro del patch. Coerentemente con l’osservazione 2 la scelta di un puntatore all’oggetto effettivo ci consente di gestire meglio la memorizzazione delle informazioni e consente anche una programmazione più agile. L’ordine di memorizzazione degli elementi degli std::vector dipende dalla struttura dati PointToSides: il costruttore di questa classe riceve un oggetto di 4 Le strutture dati 17 tipo Our_List e sfrutta le informazioni in esso contenute nel modo seguente: per ogni lato appartenente alla lista memorizziamo, seguendone l’ordine – il vertice non incidente del centro del patch; – il lato di bordo del patch associato all’elemento adiacente in senso antiorario (per il patch) al lato della lista corrente. Tra i metodi di questa classe quelli più importanti e cruciali negli algoritmi sono Patch::is_convex, Patch::is_in_patch e il virtual della classe madre measure. I primi due implementano l’algoritmo presentato nella sezione relativa al Node Movement, il terzo fa ancora riferimento alla formula (25) con la differenza che l’indice 3 è sostituito dal numero di vertici del patch corrente. 4.5 La classe Mesh Mesh è la classe principale del programma di adattazione, essa è associata alla vera e propria memorizzazione della griglia e dispone di metodi che ne consentono la modifica mediante la procedura adattiva. Dalla funzione principale (main) sarà possibile istanziare un unico oggetto di questa classe ed agire su esso fino ad ottenere una griglia soddisfacente. Prima di discutere i membri ridefiniamo per comodità alcuni tipi utilizzati necessari alla memorizzazione degli elementi della griglia appartenenti ai tre insiemi citati all’inizio di questo paragrafo. typedef std::vector<Point> point_vector_type; typedef std::vector<Edge> edge_vector_type; typedef std::vector<Triangle*> shape_vector_type; Nodi, lati e triangoli della mesh vengono memorizzati in un contenitore della Standard Template Library: gli std::vector; nello specifico abbiamo: point_vector_type M_points; edge_vector_type M_edges; shape_vector_type M_elements; E’inoltre necessario memorizzare le ulteriori strutture dati associate al patch di ogni nodo e agli elementi adiacenti ad un lato; i seguenti attributi privati contengono le informazioni associate alla PointToSides ed alla SideToElement: std::vector<Our_List> M_P2S; std::vector< UBLAS::bounded_vector<int, 2> > M_S2E; Come spiegato nella sezione relativa alla classe Our_List, la M_P2S consente uno scorrimento veloce, in corrispondenza di ogni nodo, dei lati che incidono in esso grazie alla scelta delle liste, essa sarà di dimensione NP ; la M_S2E, di dimensione pari al numero dei lati, memorizza per ogni lato un vettore a cui sono associati gli indici degli elementi adiacenti secondo la convenzione già spiegata, dato il limitato e noto numero di informazioni da conservare scegliamo gli UBLAS::bounded_vector. L’identità del lato di riferimento si identifica con l’indice di collocazione. 4 Le strutture dati 18 La scelta degli std::vector è congeniale alle nostre necessità in quanto consentono un inserimento molto efficiente degli elementi in coda ed un rapido accesso per indice; il costruttore di mesh rende chiare queste osservazioni. La costruzione avviene all’inzio del programma a seguito dell’acquisizione di un file .msh che contiene le informazioni associate ai nodi della griglia nelle loro coordinate cartesiane e gli indici dei punti associati ad ogni triangolo; queste informazioni vengono acquisite nell’ordine dato dal file .msh (e tale sarà l’ordinamento nella numerazione globale) ed inglobate nelle strutture attraverso il metodo .push_back. Per gestire in modo efficiente la memorizzazione nel corso del riempimento delle strutture riserviamo all’inizio la memoria necessaria ad ogni std::vector in quanto nota ed acquisita dal file di mesh. M_points.reserve(nP); M_edges.reserve(3*nEl); M_elements.reserve(nEl); M_S2E.reserve(3*nEl); M_P2S.reserve(nP); dove nP ed nEl sono rispettivamente NP ed NT . Tra gli attributi privati abbiamo inoltre un ulteriore contenitore (anche in questo caso un std::vector) con le informazioni riguardanti le matrici di metrica della mesh associate ad ogni nodo; essendo tali matrici Mi ∈ R2×2 ∀i = 1, ...NP simmetriche scegliamo di memorizzarle in UBLAS::bounded_vector di dimensione 3; ad ognuna di queste corrisponderà un vertice, quello la cui identità corrisponde all’indice di memorizzazione. std::vector< UBLAS::bounded_vector<Real, 3> > M_NodesMetric; Associati a questa struttura abbiamo dei metodi per la gestione delle informazioni di metrica, quali Mesh::const_Tmetric e Mesh::DET_const_Tmetric che ritornano rispettivamente la metrica costante sul triangolo a partire da quella dei sui vertici ed il determinante della matrice di metrica. Veniamo ora ai metodi fondamentali della classe Mesh che, sfruttando classi e strutture fino ad ora descritte, implementano i metodi di adattazione di interesse. 4.5.1 Node Movement Lo spostamento dei nodi viene gestito dal metodo Mesh::void adapt_by_node_movement(). Coerentemente con l’algoritmo illustrato nelle sezioni precedenti questa funzione scorre il vettore di nodi della griglia ed applica ad ogni punto la tecnica Node Movement che costruisce il Patch del nodo corrente e lo passa per riferimento al metodo Mesh::int node_movement(Patch& Ptc). Quest’ultimo riceve per riferimento come argomento un oggetto della classe Patch che viene creato nella funzione chiamante ed aggiornato in corrispondenza di ogni nodo in modo che contenga le informazioni associate al punto corrente. La scelta di non creare un vettore di oggetti Patch come attributo di Mesh nasce dalla necessità di non allocare ulteriore memoria contentente informazioni già inserite in altre strutture proprie di questa classe. Osservazione 3. Lo spostamento dei nodi si basa sulle informazioni del patch corrente e come tale può sembrare coerente implementarlo come metodo della classe Patch; nonostante questo, la necessità di accedere a dati e metodi proprio della mesh fa sı̀ che sia molto più efficiente e di facile implementazione 4 Le strutture dati 19 pensare a questo come metodo di Mesh; potremo in questo modo accedere direttamente alle informazioni relative alla metrica del patch, ai triangoli che lo formano ed ai lati che lo costituiscono. Consideriamo inoltre che in seguito alla procedura di adattazione sarà possibile aggiornare direttamente il vettore di Point nelle sue coordinate ed ottenere gratuitamente l’aggiornamento di tutte le strutture che possiedono puntatori agli oggetti modificati. Quelle associate ai nodi saranno le uniche strutture che necessitano di aggiornamento, nessuna modifica sarà dunque necessaria per strutture rimanenti. L’implementazione di questo metodo è del tutto coerente con l’algoritmo presentato nella relativa sezione, di conseguenza è necessario poter controllare la convessità del patch e la sua eventuale modifica, nel caso di patch concavo. Questi controlli sono affidati al metodo di Patch, is_convex(), ed al metodo ausiliario di Mesh, dato dalla funzione void Mesh::get_weigths4correction(std::vector<Real>& weigths, Patch& Ptc), essa riceve per riferimento un vettore di pesi (associati ai vertici del patch) ed il patch, implementa quindi l’algoritmo di correzione modificando il vettore weigths le cui componenti serviranno a spostare i vertici del patch restituendo quello ridotto secondo lo schema, già presentato, di Figura 6. Un discorso a parte va fatto per lo spostamento dei nodi di bordo. Sottolineiamo il fatto che, per semplicità, lavoreremo nell’ipotesi che la frontiera della mesh non sia curva, ma poligonale con lati paralleli agli assi cartesiani; di conseguenza lo spostamento dei nodi in questione coincide con la modifica della coordinata mobile. Ci sembra dunque opportuno implementare un metodo a sè stante associato all’adattazione di punti di bordo, Mesh::bordernode_movement(Patch& Ptc), che applica l’algoritmo di spostamento occupandosi della sola coordinata non fissa. 4.5.2 Diagonal Swapping Lo scambio di diagonali dei quadrilateri non convessi viene gestito del metodo Mesh::adapt_by_diagonal_swapping come quello precedente esso può essere direttamente chiamato dopo aver istanziato l’oggetto ed aver acquisito la metrica; coerentemente con il relativo algoritmo questo metodo cicla su tutti i lati della mesh scorrendo il vettore M_Edges come segue: for(int i=0; i<M_edges.size();++i) { ... }, Ci è sembrato però opportuno affiancare a tale scorrimento, che presenta una direzione preferenziale, un secondo ciclo che visita i lati nell’ordine inverso: for(int i=(M_edges.size())-1; i>0;--i) { ... }, in questo modo possiamo rendere più efficacie l’operazione di adattazione; entrambi i cicli appartengono ad un ciclo più esterno nel cui criterio di arresto si controllano il numero di lati scambiati (sul quale è imposto un limite) ed il fatto che nessun lato soddisfi le condizioni di scambiabilità while(((somma_esiti!=0) && (it< max_iter_numb))) 4 Le strutture dati 20 Ad ogni iterazione del ciclo for chiamiamo il metodo Mesh::single_edge_swapping che per ogni lato controlla la convessità del quadrilatero associato attraverso Mesh::is_quadCONVEX; qualora questa operazione dia esito positivo si passa al calcolo delle quantità Gain e Qual attraverso gli omonimi metodi, che ricevono rispettivamente un lato ed un elemento, nella fattispecie si tratta degli elementi adiacenti restituiti dalla M_S2E come segue int ind_elem0= int ind_elem1= M_S2E[edge_indic][0]; M_S2E[edge_indic][1]; Segue quindi il controllo sulle quantità appena citate per determinare il vantaggio dell’operazione di swapping: if((current_gain>2)||((current_gain==2)&&(std::min(Q0,Q1)<std::min(Q_new0,Q_new1)))) dove Q_new0,Q_new1 corrispondono alle qualità degli elementi E1∗ e E2∗ . In caso di esito positivo si passa all’operazione di scambio che consiste nell’effettuare delicate operazioni di aggiornamento di tutte le strutture dati: il nuovo lato introdotto acquisisce l’indice di quello eliminato e prende il suo posto nella M_Edges. Segue l’aggiornamento della M_Elements che richiede di porre particolare attenzione agli indici del quadrilatero in modo da essere aggiornata in modo coerente con le convenzioni spiegate sull’orientazione interna. Abbiamo infine l’aggiornamento della M_S2E e quello più articolato della M_P2S; in quest’ultimo caso ci appoggiamo ad alcuni metodi predefiniti nella classe std::list che consentono di cancellare elementi (in fase di eliminazione del lato scambiato) e di inserirne altri in una determinata posizione, scelta in base alla collocazione del nuovo lato all’interno dell’orientazione antioraria dei lati incidenti nel nodo (cui fa riferimento l’elemento della struttura in questione). Tali metodi sono .erase(it) .insert(it,elem) .splice(it1,list,it2,it3) dove it è l’iteratore della lista che indica la posizione di cancellazione o inserimento; it1, nel terzo metodo, è l’iteratore che indica la posizione di inserimento della sotto-lista nella lista list e it2 e it3 delimitano sotto-lista dell’oggetto chiamante da spostare in list. 4.6 Il main Nelle sezioni precedenti abbiamo visto nel dettaglio attributi e metodi delle classi C++ implementate. Il lettore si sarà accorto del fatto che esse non sopperiscono a tutte le operazioni necessarie per implementare un intero ciclo di adattazione, che prevede di associare alla procedura adattiva la risoluzione numerica ad Elementi Finiti di un problema alle derivate parziali di diffusione, trasporto e reazione. Per testare l’efficacia delle strategie implementate abbiamo infatti integrato il nostro codice permettendogli di essere supportato dal software FreeFem++ chiamato nel corso del nostro programma attraverso l’uso del comando system. I passi eseguiti dal main, che riceve dall’utente, come argomenti, il massimo numero di iterazioni per il ciclo di swap e per l’adattazione, possono essere schematizzati come segue: 4 Le strutture dati 21 1. eseguire il file main_in_FF.edp lanciando FreeFem++, che ha il compito di creare la mesh di partenza e salvarla in un file mesh_pre.msh, risolvere il problema e generare la metrica ottimale, salvata in un file di testo metric.txt (il paragrafo successivo chiarirà meglio la struttura del codice del software ausiliario). L’interazione con il programma esterno, come anticipato è gestita dal comando system il cui funzionamento è chiarito dal seguente esempio che riporta le operazioni appena descritte: char command3[60]; sprintf(command3,"FreeFem++-nw -f free/main_in_FF.edp"); system(command3); 2. leggere il file mesh_pre.msh ed invocare il costruttore della classe Mesh per creare l’oggetto m: mesh_filename="input/mesh_pre.msh"; Mesh m(mesh_filename); 3. fino a che non viene soddisfatto il criterio di arresto secondo il quale o è raggiunto un massimo numero di iterazioni o l’errore commesso è inferiore ad una tolleranza stabilita a priori, implemetare il ciclo di adattazione: (a) leggere il file di metrica corrente: metric_filename=input/metric.txt; (b) adattare mediante lo spostamento dei nodi: m.adapt_by_node_movement(); (c) ogni s iterazioni (con s fissato) adattare mediante lo scambio delle diagonali m.adapt_by_diagonal_swapping(max_iteration); (d) generare una nuova metrica e passarla a FreeFem per ricalcolare la soluzione ed i parametri ottimali m.create_MSH("input/mesh_post.msh"); char command8[60]; sprintf(command8,"FreeFem++-nw -f free/main_up_FF.edp"); system(command8); 4.7 FreeFem++ Lo scopo che si pone questo lavoro è quello di implementare le strategie di adattazione descritte nei capitoli precedenti. Per poter realizzare un ciclo completo di adattazione sono però necessarie molte altre mosse ed è per questo motivo che si è pensato di far uso del software FreeFem++ a supporto di queste ultime. In questo paragrafo spiegheremo pertanto in modo breve quali sono le fasi del ciclo di adattazione che affidiamo a FreeFem++ e quali funzioni abbiamo utilizzato per realizzarle. Per prima cosa esso genera la mesh uniforme di partenza, avvalendosi del mesh generator bamg; dopodichè possiamo schematizzare le operazioni che compie nel modo seguente: • Deduzione della metrica associata alla griglia attuale, tramite il comando adaptmesh con l’opzione nomeshgeneration=TRUE; 5 I risultati numerici 22 • Calcolo la soluzione del problema differenziale in esame, con approssimazione ad elementi finiti lineari; • Ricostruzione del gradiente della soluzione; • Calcolo della stima a posteriori dell’errore; • Calcolo della metrica ottimale basata sulla precedente stima. 5 I risultati numerici Riportiamo in questo paragrafo i casi test trattati con i relativi risultati di adattazione ottenuti. Risolviamo il problema 1 di diffusione trasporto e reazione in corrispondenza di diversi parametri nella formulazione (a, b, c e f ), di domini Ω e di condizioni al bordo sullo stesso. 1 1 1 Caso Test Double Ramp Channel Glass a 10−3 10−3 10−4 b (1, 0)t (y, −x)t (y − 21 , −x − 21 )t c 0 0 100 I(r < 15 ) ∗ Tab. 1: Parametri scelti nelle simulazioni. ∗ r è definito come f 1 0 I( 15 < r < 41 ) Ω Ω1 Ω2 Ω3 q (x − 12 )2 + (y − 12 )2 La Tabella 1 riporta, associati ad ogni caso test, i parametri determinanti i problemi in questione. 5.1 Double Ramp Risolviamo il problema 1 su un dominio bidimensionale di forma a L, caratterizzato da: 0 ≤ x ≤ 4 e 0 ≤ y ≤ 4. I parametri a, b e c sono riportati in Tabella 1, mentre per quel che riguarda le condizioni al bordo abbiamo Dirichlet omogeneo su tutta la frontiera. In questo caso la soluzione esatta del problema presenta un forte layer di bordo in corrispondenza di x = 4; meno accentuati sono i layers associati ai lati orizzontali. In modo coerente con la procedura adattiva descritta nei paragrafi precedenti ci aspettiamo un infittimento della griglia di calcolo in corrispondenza delle zone sopra evidenziate; vogliamo che lo stimatore η, fortemente dipendente dal gradiente ricostruito porti i nodi ed i lati della mesh a spostarsi coerentemente con la soluzione. Le Figure 8-13 riportano la soluzione, le mesh e le componenti del gradiente ricostruito in corrispondenza di iterazioni successive della procedura, quando il numero massimo di iterazioni consentite per il ciclo di Diagonal Swapping è 30 Le Figure 14-19 riportano invece i risultati delle simulazioni con al massimo 50 iterazioni di swap. In entrambi i casi come griglia iniziale si è scelto di discretizzare i lati più lunghi con 20 nodi, mentre i rimanenti con 18 e abbiamo stabilito un numero massimo di iterazioni del ciclo di adattazione pari a 100. I risultati ottenuti sono stati prodotti lanciando /.ADAPT_DOUBLERAMP (Per maggiori dettagli si veda il CD allegato). Abbiamo notato che il valore dello stimatore η globale rimane pressochè costante al variare delle iterazioni (a tale proposito si osservi la Figura 20 che riporta l’andamento di tale grandezza nel caso di 50 iterazioni di swapping), ma che, nonostante ciò, dopo poche iterazioni del ciclo la griglia viene adattata proprio nella direzione attesa. 5 I risultati numerici 23 soluzione 3.5 3 2.5 2 1.5 1 0.5 0 −0.5 4 4 3 3 2 2 1 1 0 0 Fig. 8: Double Ramp: griglia e soluzione al passo 1 GX GY 2 15 0 10 −2 5 −4 0 −6 −5 −8 −10 −10 −15 −12 4 −20 4 4 3 4 3 3 2 3 2 2 1 2 1 1 0 1 0 0 0 Fig. 9: Double Ramp: gradienti al passo 1 soluzione 3.5 3 2.5 2 1.5 1 0.5 0 −0.5 4 4 3 3 2 2 1 1 0 0 Fig. 10: Double Ramp: griglia e soluzione al passo 20 Si può notare che non sussiste particolare differenza tra i due tipi di simulazioni, quindi riteniamo che un numero di 30 iterazioni di Diagonal Swapping sia ragionevole, dato che non aumenta troppo il tempo di calcolo, pur permettendo sufficiente scambio di lati. Il confronto con i risultati ottenuti utilizzando la funzione adaptmesh di FreeFem++ non è del tutto 5 I risultati numerici 24 GX GY 4 30 2 20 0 10 −2 0 −4 −10 −6 −20 −8 −30 −10 −12 4 −40 4 4 3 4 3 3 2 3 2 2 1 2 1 1 0 1 0 0 0 Fig. 11: Double Ramp: gradienti al passo 20 soluzione 3.5 3 2.5 2 1.5 1 0.5 0 −0.5 4 4 3 3 2 2 1 1 0 0 Fig. 12: Double Ramp: griglia e soluzione al passo 100 GX GY 5 30 20 0 10 0 −5 −10 −20 −10 −30 −15 4 −40 4 4 3 3 2 4 3 3 2 2 1 1 0 2 1 1 0 0 0 Fig. 13: Double Ramp: gradienti al passo 100 significativo in quanto tale software prevede la possibilità di aggiungere e togliere nodi alla griglia, mosse che in modo evidente migliorano l’adattazione, ma che per ora non sono state implementate nel codice sin qui prodotto. 5 I risultati numerici 25 soluzione 3.5 3 2.5 2 1.5 1 0.5 0 −0.5 4 4 3 3 2 2 1 1 0 0 Fig. 14: Double Ramp: griglia e soluzione al passo 1, 50 GX GY 2 15 0 10 −2 5 −4 0 −6 −5 −8 −10 −10 −15 −12 4 −20 4 4 3 4 3 3 2 3 2 2 1 2 1 1 0 1 0 0 0 Fig. 15: Double Ramp: gradienti al passo 1, 50 soluzione 3.5 3 2.5 2 1.5 1 0.5 0 −0.5 4 4 3 3 2 2 1 1 0 0 Fig. 16: Double Ramp: griglia e soluzione al passo 20, 50 5.2 Channel In questo secondo esempio usiamo i parametri del Caso Test 2 in Tabella 1; il dominio è il medesimo considerato nel caso test della doppia rampa presentato in precedenza. Per quel che riguarda le condizioni al bordo, sulla frontiera x = 0 imponiamo Dirichlet non omogeneo, ovvero u = 1; sui lati 5 I risultati numerici 26 GX GY 4 30 2 20 0 10 −2 0 −4 −10 −6 −20 −8 −30 −10 −12 4 −40 4 4 3 4 3 3 2 3 2 2 1 2 1 1 0 1 0 0 0 Fig. 17: Double Ramp: gradienti al passo 20, 50 soluzione 3.5 3 2.5 2 1.5 1 0.5 0 −0.5 4 4 3 3 2 2 1 1 0 0 Fig. 18: Double Ramp: griglia e soluzione al passo 100, 50 GX GY 5 30 20 0 10 0 −5 −10 −20 −10 −30 −15 4 −40 4 4 3 3 2 4 3 3 2 2 1 1 0 0 2 1 1 0 0 Fig. 19: Double Ramp: gradienti al passo 100, 50 x = 2, y = 2 e y = 4 imponiamo condizioni di Dirichlet omogenee, mentre sulle restanti porzioni del bordo abbiamo condizioni di Neumann omogeneo. Dunque abbiamo dato al bordo discontinuo in corrispondenza dei punti angolosi (0, 2) e (0, 4); questo fatto impone sicuramente dei limiti rispetto all’accuratezza della soluzione numerica. La soluzione che ci aspettiamo presenta due layers interni di 5 I risultati numerici 27 Andamento di eta eta eta 2.25 2 1.75 1 11 21 31 41 51 iterazioni 61 71 81 91 Fig. 20: Andamento di η globale forma circolare in corrispondenza dei quali abbiamo alti valori del gradiente; di conseguenza associato a tali zone ci aspettiamo un infittimento della griglia; in corrispondenza della zona meno soggetta ad alti gradienti (in particolare quella relativa al nodo di bordo (4, 4) ci aspettiamo, al contrario, un deraffinamento della mesh. soluzione 1.2 1 0.8 0.6 0.4 0.2 0 −0.2 4 4 3 3 2 2 1 1 0 0 Fig. 21: Channel: griglia e soluzione al passo 100 Le Figure 21, 22 riportano la soluzione calcolata, la mesh e il valori dei gradienti ricostruiti in corrispondenza dell’ultima iterazione del processo adattivo. Possiamo notare intanto che il gradiente ricostruito risente del dato di Dirichlet discontinuo in corrispondenza dei punti angolosi, infatti presenta dei picchi anomali. Ci aspettiamo dunque che l’errore lı̀ non possa migliorare più di tanto con l’adattazione della griglia in quanto l’errore è intrinseco nella soluzione (quella esatta non esiste) e non dipende dallo schema numerico. I due boundary layer sono colti dall’adattazione, che infatti tende ad infittire in loro corrispondenza anche se non in modo particolarmente apprezzabile, da una parte perchè la griglia che si è scelto di utilizzare per la simulazione è relativamente lasca, dall’altra perchè il processo implementato non raffina aggiungendo nodi. In particolare la zona inferiore sembra non risentire del processo di adattazione; questo accade in quanto la tecnica di spostamento dei nodi tende a muoverli in direzioni opposte rendendo nullo il tentativo di spostamento. 5 I risultati numerici 28 GX GY 4 20 2 15 0 10 −2 5 −4 0 −6 −5 −8 −10 −10 −15 −12 −14 4 −20 4 4 3 4 3 3 2 3 2 2 1 2 1 1 0 1 0 0 0 Fig. 22: Channel: gradienti al passo 100 5.3 Glass Facciamo ora riferimento ai parametri relativi al Caso Test 3: il dominio è il quadrato (0, 1)2 , con condizioni al bordo di Dirichlet omogenee. La soluzione mostra uno strato limite interno di forma circolare in corrispondenza della zona 51 < r < 14 ed anche un forte gradiente in direzione radiale nella regione 41 < r < 1; queste saranno le regione nelle quali ci aspettiamo che lo stimatore η riconosca la necessità di infittire la griglia lasciandola invece lasca nella zona centrale e nelle zone di corner. soluzione 8 7 6 5 4 3 2 1 0 −1 1 0.8 1 0.6 0.8 0.6 0.4 0.4 0.2 0.2 0 0 Fig. 23: Glass: griglia e soluzione al passo 1 La mesh che abbiamo utilizzato è stata discretizzata con 30 nodi per lato; nella sequenza di Figure 23-28 possiamo notare che in questo caso già alla decima iterazione (dunque con l’ausilio del solo movimento dei nodi) la griglia adattata coglie lo strato limite circolare. Dall’osservazione delle mesh ottenute notiamo un insolito andamento nelle zone di corner dove il profilo circolare non viene colto secondo l’andamento corretto: i nodi vengono coerentemente spinti verso il centro ma l’orientazione ottenuta non è in linea con quella della soluzione esatta; associamo questo fatto all’implementazione di sole due tecniche di adattazione. 6 Conclusioni 29 GX GY 150 150 100 100 50 50 0 0 −50 −50 −100 −100 −150 1 −150 1 0.8 0.8 1 0.6 1 0.6 0.8 0.8 0.6 0.4 0.4 0.2 0.4 0.2 0.2 0 0.6 0.4 0.2 0 0 0 Fig. 24: Glass: gradienti al passo 1 soluzione 8 7 6 5 4 3 2 1 0 −1 1 0.8 1 0.6 0.8 0.6 0.4 0.4 0.2 0.2 0 0 Fig. 25: Glass: griglia e soluzione al passo 10 GX GY 300 300 200 200 100 100 0 0 −100 −100 −200 −200 −300 1 −300 1 0.8 1 0.6 0.8 0.8 1 0.6 0.6 0.4 0.4 0.2 0.4 0.2 0.2 0 0.8 0.6 0.4 0.2 0 0 0 Fig. 26: Glass: gradienti al passo 10 6 Conclusioni Alla fine del lavoro svolto è opportuno fare alcune considerazioni finali illustrando e mettendo in evidenza i punti forti del codice presentato e suoi eventuali sviluppi ed estensioni. Riassumiamo di 6 Conclusioni 30 soluzione 4 3.5 3 2.5 2 1.5 1 0.5 0 −0.5 1 0.8 1 0.6 0.8 0.6 0.4 0.4 0.2 0.2 0 0 Fig. 27: Glass: griglia e soluzione al passo 100 GY GX 300 80 60 200 40 100 20 0 0 −20 −100 −40 −200 −60 −300 1 −80 1 0.8 1 0.6 0.8 0.6 0.4 0.8 1 0.6 0.8 0.2 0.4 0.2 0.2 0 0.6 0.4 0.4 0.2 0 0 0 Fig. 28: Glass: gradienti al passo 100 seguito tali osservazioni. • Il presente lavoro considera, come già sottolineato più volte, due sole strategie di adattazione; esse sono tali da non aggiungere nè togliere elementi alla griglia. Dal punto di vista della qualità della procedura adattiva questo è penalizzante, l’introduzione di nuovi nodi e l’eventuale rimozione di lati in eccesso comporta risultati molto soddisfacenti e consente un mglior infittimento (o deraffinamento) della mesh di calcolo restituendo di conseguenza soluzioni più accurate. Alla luce dei risultati ottenuti, possiamo dunque affermare che la procedura adattiva basata sul solo spostamento dei nodi e scambio delle diagonali ha dato risultati buoni e soprattutto coerenti con la stima dell’errore che avevamo a disposizione. Le zone che maggiormente necessitavano adattazione sono state colte e dall’osservazione delle griglie ottenute possiamo riconoscere l’andamento della soluzione esatta del problema. • Associata all’osservazione precedente vi è il fatto che tale procedure adattive risultano particolarmente veloci; un rapido confronto con l’adattazione implementata dalla funzione adaptmesh, predefinita nel software FreeFem++, mette in luce tempi di calcolo competitivi ed il più delle volte vantaggiosi (ricordiamo però che tale software implementa anche l’eliminazione di lati e l’aggiunta 6 Conclusioni 31 di nuovi nodi, associata a tempi di calcolo più onerosi vi è anche una diversa qualità della griglia ottenuta e, di conseguenza, della soluzione). • Insistiamo sull’uso del software FreeFem++: esso potrebbe sembrare una limitazione o un punto debole del programma costretto a chiamare in causa sftware esterni; in realtà questo è un aspetto che nasconde il vantaggio di poter sfruttare diverse tecniche di acquisizione della metrica ottimale, non necessariamente basate su una stima a posteriori dell’errore che sfrutta la ricostruzione del gradiente, quale quella da noi utilizzata. L’uso del comando system rende inoltre molto efficiente l’interazione con programmi esterni. • Un’ultima osservazione riguarda l’uso dei tradizionali puntatori: essi sono stati utilizzati nella gestione dei nodi della griglia. Sono disponibili tipi di puntatori molto più efficienti, detti Smart Pointers, essi consentono di gestire al meglio le operazioni di inizializzazione e distruzione dei puntatori stessi; la struttura del nostro codice non ci ha però permesso di utilizzarli per la loro incompatibilità con i contenitori della Standard Template Library di cui abbiamo fatto largo uso Da ultimo lasciamo spazio ad eventuali sviluppi futuri del codice presentato che si delinea solo come versione preliminare di adattatore di griglia aperto a modifiche ed estensioni. Come già sottolineato il codice si presenta aperto all’interazione con altri software che permettano di generare parametri ottimali sulla base dei quali implementare la procedura adattiva. Sempre nella prospettiva di flessibilità del programma creato, l’aver implementato la classe astratta Shape consente un eventuale gestione di mesh non solo triangolari ma, ad esempio, formate da quadrilateri. L’aspetto fondamentale è la possibile integrazione del codice con altre importanti strategie di adattazione che segnerebbero sicuramente un netto miglioramento nella qualità delle mesh ottenute, come già sottolineato l’eliminazione di lati e l’aggiunta di punti da un contributo non indifferente al processo di adattazione. Le strutture introdotte si prestano anche a questo tipo di operazioni, che comporterebbero però sicuramente un incremento degli oneri computazionali. Riferimenti bibliografici [1] L. Formaggia,S. Perotto: New anisotropic a priori error estimates. Numer.Math., 89, 641-667 (2001) [2] L. Formaggia,S. Perotto: Anisotropic error estimates for elliptic problems. Numer.Math., 94, 67-92 (2003) [3] L. Formaggia, S. Micheletti, S. Perotto: Anisotropic mesh adaptation in computational fluid dynamics:Application to the advection-diffusion-reaction and the Stokes problems., Applied Numer. Math., 51, 511-533 (2004) [4] S. Micheletti, S. Perotto : Reliability and efficiency of an anisotropic Zienkiewicz-Zhu error estimator., Computer Methods in Applied Mechanics and Engineering, Vol. 195, Issues 9-12, 799-835 (2006) 6 Conclusioni 32 [5] D. Yang: C++ and Object Oriented Numeric Computing for Scientists and Engineers [6] N. M. Josuttis: The C++ Standard Library: A Tutorial and Reference, AWL Publisher, (1999) [7] M. D’Elia, M. Dalla Rosa: Adattazione di griglia tramite stimatori anisotropi a posteriori alla Zienkiewicz-Zhu per un problema di diffusione, trasporto e reazione, (2006) [8] L. Formaggia: Two Dimensional Mesh Adaptation For Unstructured Grids