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