POLITECNICO DI MILANO Corso di Laurea Specialistica in Ingegneria Matematica Orientamento Calcolo Scientifico PROGRAMMAZIONE AVANZATA per il CALCOLO SCIENTIFICO PROGETTO: Proiezione di punti su una superficie triangolata. progetto di: Elisa Maculan Matr. 679979 Anno Accademico 2006-2007 Indice 1 Introduzione 2 2 Classi e strutture 2.1 Struct Point . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Class Triangle . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Class Mesh . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 5 8 3 Funzioni e metodi 10 3.1 Proiezione di un punto su un piano . . . . . . . . . . . . . . . 10 3.2 Appartenenza di un punto ad un triangolo . . . . . . . . . . . 14 4 Ricerca del triangolo stl di appartenenza della proiezione di ogni nodo della mesh netgen 4.1 Metodo brute force . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Metodo con ricerca del nodo stl più vicino al nodo netgen . . 4.3 Metodo con algoritmo di ricerca del nearest neighbor . . . . . 21 21 23 25 5 Applicazioni e problemi riscontrati 28 6 Algoritmi alternativi 31 1 1 Introduzione Date una mesh netgen ed una superficie triangolata stl, l’obiettivo di questo lavoro è quello di trovare per ogni nodo della mesh netgen i tre nodi più vicini, nello spazio 3D, della superficie stl. Le due mesh sono fornite a priori e l’idea che si desidera sviluppare è quella di cercare per ogni nodo netgen il triangolo stl a cui appartiene la sua proiezione, dato che il nodo netgen può non giacere sul piano definito da un triangolo stl. La ricerca dei tre nodi stl più vicini ad ogni nodo netgen è utile in quanto per ogni nodo stl sono noti i suoi spostamenti in dieci istanti temporali diversi. Perciò, una volta individuati i tre nodi stl più vicini ad un nodo netgen ed interpolati i loro spostamenti, si riesce a calcolare anche lo spostamento del nodo netgen e quindi a ricostruire la mesh netgen ad ogni istante temporale. Questa mesh verrà usata per il calcolo fluidodinamico d’interazione fluido-struttura, però con struttura nota. Per spiegare il procedimento seguito per il raggiungimento dell’obiettivo, questo lavoro si articola in paragrafi nel seguente modo: • Paragrafo 2: CLASSI E STRUTTURE Breve descrizione della struttura Point e delle classi Triangle e Mesh, necessarie per la lettura della mesh netgen e della superficie triangolata stl di partenza. • Paragrafo 3: FUNZIONI E METODI Descrizione delle idee teoriche alla base della funzione esterna di proiezione di un punto sul piano individuato da un triangolo e del metodo della classe Triangle che verifica l’appartenenza di un punto ad un triangolo, quando giacciono sullo stesso piano. • Paragrafo 4: RICERCA DEL TRIANGOLO STL DI APPARTENENZA DELLA PROIEZIONE DI OGNI NODO NETGEN Descrizione di tre metodi differenti per la ricerca del triangolo stl di appartenenza. Il primo consiste in un metodo brute force che cicla su tutti i triangoli della superficie stl, calcolando la proiezione di ogni nodo netgen sul piano individuato da ciascun triangolo e l’appartenenza della proiezione stessa ad ogni triangolo stl. Il secondo, invece, cicla su tutti i nodi stl per cercare quello più vicino ad ogni nodo netgen; in seguito calcola proiezione e sua appartenenza ad ogni triangolo nel 2 patch del nodo stl più vicino. Il terzo esegue le stesse operazioni del secondo, ma rende più veloce ed efficiente la ricerca del nodo stl più vicino sfruttando un algoritmo di ricerca del nearest neighbor di una libreria esterna, la libreria ANN. • Paragrafo 5: APPLICAZIONI E PROBLEMI RISCONTRATI Applicazione dei tre metodi ad un caso test semplice con mesh costituite da pochissimi nodi (cilindro) e ad un caso reale più complesso (arco aortico). Analisi dei problemi riscontrati eseguendo il codice implementato nel caso reale con geometria poco regolare. • Paragrafo 6: ALGORITMI ALTERNATIVI Descrizione di due algoritmi alternativi, implementati per risolvere i problemi descritti nel paragrafo precedente. Il primo algoritmo sfrutta la libreria ANN per la ricerca diretta dei tre nearest neighbor di ogni nodo netgen; il secondo calcola il baricentro di ogni triangolo stl, ricerca il baricentro più vicino ad ogni nodo netgen e considera i vertici del triangolo con questo baricentro come i tre punti stl più vicini al nodo netgen. 2 Classi e strutture In questo paragrafo introduciamo brevemente attributi e metodi delle strutture di base implementate, necessarie per la lettura della mesh netgen e della superficie stl note e per i calcoli successivi. 2.1 Struct Point Per leggere i nodi di una mesh e i punti 3D si è deciso di usare una struttura in modo tale da avere degli attributi pubblici a cui poter accedere direttamente, senza bisogno di metodi appositi. Questo perchè si è scelto di richiamare gli attributi in modo diretto anche nella funzione esterna di proiezione, ma naturalmente è possibile effettuare altre scelte. La struttura Point è dotata di quattro attributi: double x; double y; double z; che rappresentano le tre coordinate spaziali 3 int ID; che rappresenta un indicatore del nodo nella mesh ed è posto = −1 di default per punti 3D che non appartengono ad alcuna mesh. Per questa struttura vengono forniti diversi costruttori: //costruttore di default Point():x(0.0),y(0.0),z(0.0),ID(-1) {}; //costruttore che legge da file Point(char* fileName,int num):ID(-1) {}; //costruttore a cui passo tre numeri in ingresso Point(const double a, const double b, const double c): x(a),y(b),z(c),ID(-1) {}; //costruttore di copia Point(const Point& P): x(P.x),y(P.y),z(P.z),ID(P.ID) {}; . Il distruttore non viene sovrascritto, dato che risulta essere già adatto quello di default. Questo, infatti, distrugge i membri della classe, che nel nostro caso sono di tipo nativo, e quindi vengono distrutti in modo adeguato dal compilatore. Infine si ha l’overloading degli operatori =, <<, ==, ! = Point& operator=(const Point& P) {x=P.x; y=P.y; z=P.z; ID=P.ID;} friend ostream& operator<<(ostream& s, const Point& P) {s<<P.x<<" "<<P.y<<" "<<P.z<<" "<<P.ID; return s;} friend bool const operator==(const Point& P1, const Point& P2) { if(P1.x==P2.x && P1.y==P2.y && P1.z==P2.z) return true; 4 else return false; } friend bool const operator!=(const Point& P1, const Point& P2) { if(P1.x!=P2.x && P1.y!=P2.y && P1.z!=P2.z) return true; else return false; } . 2.2 Class Triangle Questa classe può rappresentare un triangolo di una mesh oppure un qualsiasi triangolo posizionato in uno spazio 3D. I suoi attributi sono i seguenti: Vertices vertici; un vettore di tre punti che rappresenta i tre vertici del triangolo Point normale; che rappresenta la normale al triangolo (in realtà questa sarebbe un vettore, ma in uno spazio 3D risulta comunque individuata da tre coordinate spaziali, l’ID è posto = −1 di default) int ID; che rappresenta un indicatore del triangolo nella mesh ed è posto = −1 di default per triangoli 3D che non appartengono ad alcuna mesh. Per quanto riguarda i metodi, ci sono diversi costruttori: //costruttore che legge da file Triangle(char* fileName,int num):ID(-1) {}; //costruttore che riceve tre punti in ingresso + la normale Triangle(const Point& N, const Point& P1, const Point& P2, const Point& P3):normale(N),ID(-1) { vertici.push_back(P1); 5 vertici.push_back(P2); vertici.push_back(P3); } //costruttore che riceve tre punti in ingresso Triangle(const Point& P1, const Point& P2, const Point& P3):ID(-1) { vertici.push_back(P1); vertici.push_back(P2); vertici.push_back(P3); normale = this->calcolanormale(); } //costruttore che riceve in ingresso un vettore di vertici+la normale Triangle(const Point& N,const Vertices& v):normale(N),ID(-1) { if(v.size()==3) { vertici.push_back(v.at(0)); vertici.push_back(v.at(1)); vertici.push_back(v.at(2));} else {cout<<"dimensione del vettore di vertici passato deve essere = 3";} } //costruttore di copia Triangle(const Triangle& T): normale(T.normale),ID(T.ID) { vertici.push_back(T.vertici.at(0)); vertici.push_back(T.vertici.at(1)); vertici.push_back(T.vertici.at(2));} e per il distruttore viene usato ancora quello di default. Vi sono metodi per accedere agli attributi privati //metodo che ritorna il vettore di vertici vector<Point> getvertici() const {return vertici;} //metodo che ritorna un vertice specifico Point getvertice(int i) const {return vertici.at(i);} 6 //metodo che ritorna la normale Point getnormale() const {return normale;} //metodo che ritorna l’ID int getID() const {return ID;} metodi per assegnare il valore agli attributi privati //metodo che setta l’ID void setID(int i) {ID=i;} //metodo che setta la normale void setnormale(Point N) {normale=N;} metodi per calcolare attributi o grandezze caratteristiche del triangolo //metodo che calcola il versore normale dati i vertici del triangolo Point calcolanormale() { double a,b,c; a=(vertici.at(1).y-vertici.at(0).y)*(vertici.at(2).z-vertici.at(0).z) -(vertici.at(1).z-vertici.at(0).z)*(vertici.at(2).y-vertici.at(0).y); b=(vertici.at(1).z-vertici.at(0).z)*(vertici.at(2).x-vertici.at(0).x) -(vertici.at(1).x-vertici.at(0).x)*(vertici.at(2).z-vertici.at(0).z); c=(vertici.at(1).x-vertici.at(0).x)*(vertici.at(2).y-vertici.at(0).y) -(vertici.at(1).y-vertici.at(0).y)*(vertici.at(2).x-vertici.at(0).x); //calcolo il versore normale Point N; N.x=a/sqrt(a*a+b*b+c*c); N.y=b/sqrt(a*a+b*b+c*c); N.z=c/sqrt(a*a+b*b+c*c); return N; } //metodo che calcola il baricentro del triangolo dati i vertici Point baricentro() { double xb,yb,zb; xb=(vertici.at(0).x+vertici.at(1).x+vertici.at(2).x)/3; yb=(vertici.at(0).y+vertici.at(1).y+vertici.at(2).y)/3; 7 zb=(vertici.at(0).z+vertici.at(1).z+vertici.at(2).z)/3; Point B(xb,yb,zb); return B; } ed infine sono implementati i metodi bool appartTriangle(const Point & P) {}; bool proiezandappart(const Point & P) {}; bool proiezandappart(const Point & P,double & dist) {}; bool proiezandappart(const Point & P,double & dist, const Point & N){}; che saranno descritti più precisamente nel paragrafo successivo. Anche per questa classe vengono sovrascritti gli operatori = e << friend ostream& operator<<(ostream& s, const Triangle& T) {s<<"triangolo: "<<endl<<" vertice 1: "<<T.vertici[0]<<"\n vertice 2: <<T.vertici[1]<<"\n vertice 3: "<<T.vertici[2]<<"\n normale: "<< T.normale<<"\n ID: "<<T.ID<<endl; return s;} Triangle& operator=(const Triangle& T) {vertici.push_back(T.vertici.at(0)); vertici.push_back(T.vertici.at(1)); vertici.push_back(T.vertici.at(2)); normale=T.normale; ID=T.ID;} . 2.3 Class Mesh Questa classe rappresenta naturalmente una mesh ed è dotata di due soli attributi privati: Nodes Mnodi; vettore di punti che contiene tutti i nodi della mesh 8 " Elements Mtriang; vettore dei triangoli superficiali della mesh (per la mesh netgen non è definito perchè questa mesh non contiene informazioni sui triangoli). Gli unici costruttori forniti leggono dal file.neu per costruire la mesh netgen e dal file.stl per costruire la mesh stl: //costruttore che legge da file.neu Mesh(char* fileName, int ns, int es) {}; //costruttore che legge da file.stl Mesh(char* fileName) {}; . Questo secondo costruttore sfrutta anche le informazioni provenienti dal file.dat, se questo è disponibile, risultando però molto lento; per questo scrive la mesh in un file.txt più strutturato e veloce da leggere che viene usato tutte le volte successive in cui viene eseguito il codice. Ci sono poi metodi che ritornano il numero di nodi o di triangoli ed un nodo o un triangolo specifico: //numero di punti inline int nP() const {return Mnodi.size();} //numero di triangoli (elementi di superficie) inline int nT() const {return Mtriang.size();} //referenza al nodo i-esimo (solo lettura) inline const Point& nodo(int i) const {return Mnodi.at(i);} //referenza al triangolo i-esimo (solo lettura) inline Triangle& triangolo(int i) {return Mtriang.at(i);} //vettore di nodi inline Nodes nodi() {return Mnodi;} . Il metodo vector<list<int> > patch; vector<list<int> > intornopunto() { patch.resize(Mnodi.size()); 9 for(int j=0;j<Mtriang.size();++j) { int idt=this->Mtriang.at(j).getID(); for(int k=0;k<=2;++k) { int i=this->Mtriang.at(j).getvertice(k).ID; patch.at(i).push_back(idt); } } return patch; } serve per costruire il patch di ogni nodo della mesh, contenuto poi nella lista situata nel vettore alla posizione corrispondente all’ID del nodo. Per questa classe si ha solo l’overloading dell’operatore << friend ostream& operator<<(ostream& s, const Mesh& M) {int mn=M.Mnodi.size(); int mt=M.Mtriang.size(); s<<"vettore di "<<mn<<" punti:"<<endl; for(int h=0;h<mn;++h){cout<<M.Mnodi.at(h)<<endl;} s<<"vettore di "<<mt<<" triangoli:"<<endl; for(int k=0;k<mt;++k){cout<<M.Mtriang.at(k)<<endl;} return s;} . 3 Funzioni e metodi In questo paragrafo introduciamo in modo più accurato la funzione che calcola la proiezione di un punto sul piano individuato da un triangolo e il metodo della classe Triangle che verifica l’appartenenza di un punto ad un triangolo. In particolar modo descriviamo dettagliatamente le idee che sono alla base di queste due funzioni, affrontate in entrambi i casi sfruttando la geometria analitica. 3.1 Proiezione di un punto su un piano Noti un punto P di coordinate (xp , yp , zp ) ed un triangolo con vertici P1 = (x1 , y1 , z1 ) P2 = (x2 , y2 , z2 ) P3 = (x3 , y3 , z3 ), calcoliamo la proiezione di P , Pp , sul piano individuato dal triangolo: Ax + By + Cz + D = 0. 10 Figura 1: Proiezione del punto P sul piano individuato dal triangolo di vertici P1 P2 P3 . Dalla geometria analitica sappiamo che il piano passante per tre punti è dato da: x − x1 y − y1 z − z1 det x2 − x1 y2 − y1 z2 − z1 = 0 x3 − x1 y3 − y1 z3 − z1 risolvendo si ottiene che: A = (y2 − y1 )(z3 − z1 ) − (z2 − z1 )(y3 − y1 ) B = (z2 − z1 )(x3 − x1 ) − (x2 − x1 )(z3 − z1 ) C = (x2 − x1 )(y3 − y1 ) − (y2 − y1 )(x3 − x1 ) D = −x1 A − y1 B − z1 C. Calcolando i vettori P2 P1 = (x2 −x1 , y2 −y1 , z2 −z1 ) e P3 P1 = (x3 −x1 , y3 −y1 , z3 −z1 ) e facendone il prodotto vettoriale otteniamo il vettore normale al piano n = (A, B, C). Dalla figura 1, il vettore u perpendicolare al piano e passante per P sarà un multiplo di n (u = an) ed il suo modulo sarà dato esattamente dalla distanza tra P e il piano che, sempre per la geometria analitica, vale: Axp + Byp + Czp + D . d= √ A2 + B 2 + C 2 11 Quindi a|n| = d sarà data da: ⇒ a = Axp +Byp +Czp +D . A2 +B 2 +C 2 Perciò la nostra proiezione Pp = (xp − aA, yp − aB, zp − aC) dato che Pp = P −u. Si può osservare che, calcolando la proiezione in questo modo, non è necessario conoscere il verso in cui è diretta la normale; infatti la costante moltiplicativa a può assumere segno qualsiasi, adattando cosı̀ automaticamente il calcolo delle coordinate della proiezione al verso della normale stessa. Alternativamente la proiezione può essere calcolata con la seguente formula: PP = P − ((P − P1 ) · nn )nn dove nn è il versore normale al triangolo. La funzione Point proiezione(const Point& P1, const Point& P2, const Point& P3, const Point& P) {}; implementa esattamente entrambe queste strategie. Point proiezione(const Point& P1, const Point& P2, const Point& P3, const Point& P) {//coefficienti del piano su cui giace il triangolo double a,b,c,d; a=(P2.y-P1.y)*(P3.z-P1.z)-(P2.z-P1.z)*(P3.y-P1.y); cout<<"a: "<<a<<endl; b=(P2.z-P1.z)*(P3.x-P1.x)-(P2.x-P1.x)*(P3.z-P1.z); cout<<"b: "<<b<<endl; c=(P2.x-P1.x)*(P3.y-P1.y)-(P2.y-P1.y)*(P3.x-P1.x); cout<<"c: "<<c<<endl; d=-P1.x*a-P1.y*b-P1.z*c; cout<<"d: "<<d<<endl; //versore normale Point N; N.x=a/sqrt(a*a+b*b+c*c); N.y=b/sqrt(a*a+b*b+c*c); N.z=c/sqrt(a*a+b*b+c*c); cout<<"normale: "<<N<<endl; //introduco la costante di moltiplicazione double cm; //cm=(a*P.x+b*P.y+c*P.z+d)/(a*a+b*b+c*c); cout<<"cm: "<<cm<<endl; cm=(P.x-P1.x)*N.x+(P.y-P1.y)*N.y+(P.z-P1.z)*N.z; //calcolo le tre coordinate del punto di proiezione double x1,y1,z1; 12 //x1=P.x-cm*a; //y1=P.y-cm*b; //z1=P.z-cm*c; x1=P.x-cm*N.x; y1=P.y-cm*N.y; z1=P.z-cm*N.z; //controllo che la proiezione appartenga al piano cout<<"la proiezione appartiene al piano?"<<endl; double temp=a*x1+b*y1+c*z1+d; cout<<temp<<endl; Point Pro(x1,y1,z1); return Pro; } Per verificare la correttezza di queste strategie sono stati considerati due esempi semplici, calcolabili anche a mano. Nel primo caso si calcolano le proiezioni dei punti P = (2, 5, 3), P 0 = (3, 6, 5), P 00 = (3, 6, 1) sul piano individuato dal triangolo di vertici P1 = (3, 6, 9), P2 = (4, 2, 3) e P3 = (5, 7, 8). I risultati ottenuti, PP = (3.75497, 3.06954, 4.57947), PP0 = (4.19205, 4.6887, 6.07285), PP00 = (5.38411, 3.37748, 3.1458), coincidono esattamente coi calcoli fatti a mano e sono visualizzati in figura 2. Figura 2: Primo esempio semplice: visualizzazione dei punti, delle loro proiezioni e del triangolo. 13 Nel secondo caso P = (5, 3, 2), P1 = (1, 4, 8), P2 = (2, 3, 6) e P3 = (9, 1, 2). Anche per questo esempio il risultato, PP = (5, 1.4, 2.8), coincide perfettamente con il valore calcolato a mano ed è riportato in figura 3. Figura 3: Secondo esempio semplice: visualizzazione del punto, della sua proiezione e del triangolo. 3.2 Appartenenza di un punto ad un triangolo Dati un punto P = (xp , yp , zp ) ed un triangolo di vertici P1 = (x1 , y1 , z1 ) P2 = (x2 , y2 , z2 ) P3 = (x3 , y3 , z3 ) che giacciono sullo stesso piano, vogliamo verificare se P appartiene al triangolo oppure no. Come prima idea ci riconduciamo da un problema 3D ad uno 2D, riscrivendo le coordinate dei punti rispetto ad un sistema di riferimento con origine in P1 , asse x diretto come il vettore P2 P1 e asse y perpendicolare; i versori degli assi del nuovo sistema di riferimento sono quindi: ex = P2 P1 |P2 P1 | ez = nn ey = nn × ex = (nny exz − nnz exy )i + (nnz exx − nnx exz )j + (nnx exy − nny exx )k. 14 Figura 4: Dati un punto P ed un triangolo di vertici P1 P2 P3 che giacciono sullo stesso piano vogliamo verificare l’appartenenza del punto al triangolo. Le nuove coordinate di P1 e P2 sono facili da determinare: P˜1 = (x˜1 , y˜1 ) = (0, 0) p l = |P2 P1 | = (x2 − x1 )2 + (y2 − y1 )2 + (z2 − z1 )2 P˜2 = (x˜2 , y˜2 ) = (l, 0). mentre le nuove coordinate di P3 e P sono date da: P˜3 = (x˜3 , y˜3 ) con x˜3 = P3 · ex y˜3 = P3 · ey P̃ = (x˜p , y˜p ) con x˜p = P · ex y˜p = P · ey Riscrivendo P̃ come combinazione lineare dei vertici del triangolo otteniamo x˜p = λ1 x˜1 + λ2 x˜2 + λ3 x˜3 y˜p = λ1 y˜1 + λ2 y˜2 + λ3 y˜3 λ 1 + λ 2 + λ3 = 1 15 λ3 = dato che x˜1 = y˜1 = y˜2 = 0 ⇒ y˜p y˜3 x˜p −λ3 x˜3 x˜2 . λ2 = λ 1 = 1 − λ 2 − λ3 0 6 λ1 6 1 P̃ è combinazione convessa dei vertici del triangolo ⇒ Se 0 6 λ2 6 1 e appartiene al triangolo stesso. 0 6 λ3 6 1 Esiste un’altra possibile strategia per verificare l’appartenenza di un punto ad un triangolo. Una volta ricondotto il problema 3D ad un problema 2D e riscritte le coordinate dei punti, è possibile implementare l’idea seguente. Si scrive la retta passante per P1 e P2 , cioè per un lato del triangolo: y 2 − y1 y 2 − y1 x − y − y1 − x1 = 0. x2 − x1 x2 − x1 Si vanno a sostituire nell’equazione della retta le coordinate del terzo vertice, P3 , e del punto P e si osserva se si ottengono quantità di segno uguale, il che implica che P3 e P giacciono dalla stessa parte rispetto alla retta, o opposto. In seguito si eseguono gli stessi calcoli per gli altri due lati. Se il terzo vertice e P giacciono sempre dalla stessa parte rispetto a tutti e tre i lati, allora il punto P appartiene al triangolo. Infine, una terza strategia plausibile analizza direttamente il problema 3D. L’idea è all’incirca la stessa della strategia precedente, ma, al posto di prendere in considerazione le rette passanti per i lati del triangolo, si calcolano i piani individuati dai vettori direzionati come i lati e come la normale al triangolo. Cerchiamo di spiegare più dettagliatamente questo metodo. Nello spazio 3D un piano può essere individuato da due vettori ortogonali che giacciono su di esso, oppure da un terzo vettore ortogonale ai primi due, le cui coordinate danno i coefficienti del piano stesso. Se infatti consideriamo il piano generico Ãx + B̃y + C̃z + D̃ = 0, il vettore (Ã, B̃, C̃) è ortogonale al piano. Perciò, conoscendo il vettore ortogonale al piano ed un punto che appartiene ad esso è facile calcolare i coefficienti dell’equazione del piano stesso. Nel nostro caso, conoscendo i vertici del triangolo possiamo calcolare il versore normale al triangolo nn come spiegato nel paragrafo riguardante la proiezione di un punto. Inoltre possiamo facilmente ottenere i vettori diretti come i lati P2 P1 , P3 P1 e P3 P2 . A questo punto possiamo calcolare i coefficienti del piano definito, ad esempio, dai vettori P2 P1 e nn . Il vettore 16 v ortogonale al piano sarà dato da: v = nn × P2 P1 = (nny (z2 − z1 ) − nnz (y2 − y1 ))i+ +(nnz (x2 − x1 ) − nnx (z2 − z1 ))j + (nnx (y2 − y1 ) − nny (x2 − x1 ))k. I primi coefficienti del piano saranno quindi: à = vx , B̃ = vy , C̃ = vz . Il fatto che P1 appartenga al piano che si desidera ottenere permette di calcolare anche l’ultimo coefficiente: D̃ = −x1 Ã−y1 B̃ −z1 C̃. A questo punto, avendo a disposizione l’equazione del piano, vi sostituiamo le coordinate del terzo vertice del triangolo, P3 , e del punto P e vediamo se otteniamo quantità di ugual segno. Ripetiamo lo stesso ragionamento per i piani individuati da P3 P1 e nn à = nny (z3 − z1 ) − nnz (y3 − y1 ) B̃ = nnz (x3 − x1 ) − nnx (z3 − z1 ) C̃ = nnx (y3 − y1 ) − nny (x3 − x1 ) D̃ = −x1 à − y1 B̃ − z1 C̃ e da P3 P2 e nn à = nny (z3 − z2 ) − nnz (y3 − y2 ) B̃ = nnz (x3 − x2 ) − nnx (z3 − z2 ) C̃ = nnx (y3 − y2 ) − nny (x3 − x2 ) D̃ = −x2 à − y2 B̃ − z2 C̃. Come nella seconda strategia, se in tutti e tre i casi le quantità ottenute hanno segno uguale il punto P appartiene al triangolo, altrimenti è esterno ad esso. Tutte queste strategie sono state implementate dal metodo della classe Triangle bool appartTriangle(const Point & P) {}; . Per verificare il loro corretto funzionamento sono stati usati gli stessi esempi semplici considerati nel paragrafo precedente. I risultati ottenuti sono riportati nelle figure 2 e 3 e sono facilmente verificabili a mano. Tutte e tre i metodi sono teoricamente equivalenti ed in questi casi molto semplici con coordinate intere restituiscono gli stessi risultati. Quando, però, vengono applicate a casi più complessi, come quelli descritti nel paragrafo 5, le prime due strategie, che si riconducono ad un problema 2D, non sono robuste e 17 restituiscono risultati errati (vedere figura 5), mentre la terza, che si basa sul problema 3D, funziona correttamente (vedere figura 6). Figura 5: Alcuni nodi netgen con le loro proiezioni ed i triangoli stl di appartenenza per il caso test del cilindro. Come si può osservare alcune proiezioni in realtà non appartengono al triangolo a cui, secondo il codice implementato, dovrebbero appartenere. Si può quindi affermare che, per casi abbastanza complessi, il codice con le prime due strategie non è robusto e restituisce risultati scorretti. Riportiamo il codice più robusto: //metodo che sfrutta i piani passanti per i lati in 3D bool appartTriangle(const Point & P) { //introduco delle variabili ausiliarie per facilità di scrittura double x1=vertici.at(0).x; //cout<<"x1: "<<x1<<endl; double y1=vertici.at(0).y; //cout<<"y1: "<<y1<<endl; double z1=vertici.at(0).z; //cout<<"z1: "<<z1<<endl; double x2=vertici.at(1).x; //cout<<"x2: "<<x2<<endl; double y2=vertici.at(1).y; //cout<<"y2: "<<y2<<endl; double z2=vertici.at(1).z; //cout<<"z2: "<<z2<<endl; double x3=vertici.at(2).x; //cout<<"x3: "<<x3<<endl; double y3=vertici.at(2).y; //cout<<"y3: "<<y3<<endl; double z3=vertici.at(2).z; //cout<<"z3: "<<z3<<endl; if((P.x==x1 && P.y==y1 && P.z==z1) || (P.x==x2 && P.y==y2 && P.z==z2) 18 Figura 6: Alcuni nodi netgen con le loro proiezioni ed i triangoli stl di appartenenza ottenuti con la terza strategia per il caso test del cilindro (prime 4 immagini) e per il caso reale dell’aorta (ultime 4 immagini). Si può notare che tutte le proiezioni appartengono ai triangoli e quindi il codice implementato fornisce risultati corretti. || (P.x==x3 && P.y==y3 && P.z==z3)) {return true;} //il punto è uguale ad un vertice del triangolo else {//versore normale 19 Point N=this->calcolanormale(); cout<<N<<endl; //coefficienti dei piani passanti per i lati (ax+by+cz+d=0) //piano per P1 e P2 double a,b,c,d; a=N.y*(z2-z1)-N.z*(y2-y1); b=N.z*(x2-x1)-N.x*(z2-z1); c=N.x*(y2-y1)-N.y*(x2-x1); d=-a*x1-b*y1-c*z1; //piano per P1 e P3 double a1,b1,c1,d1; a1=N.y*(z3-z1)-N.z*(y3-y1); b1=N.z*(x3-x1)-N.x*(z3-z1); c1=N.x*(y3-y1)-N.y*(x3-x1); d1=-a1*x1-b1*y1-c1*z1; //piano per P2 e P3 double a2,b2,c2,d2; a2=N.y*(z3-z2)-N.z*(y3-y2); b2=N.z*(x3-x2)-N.x*(z3-z2); c2=N.x*(y3-y2)-N.y*(x3-x2); d2=-a2*x2-b2*y2-c2*z2; //introduciamo nelle equazioni dei piani le coordinate del terzo vertice //e del punto P ed andiamo a vedere se otteniamo valori con lo stesso //segno per ogni piano. double r12p, r12p3, r23p, r23p1, r13p, r13p2; r12p=a*P.x+b*P.y+c*P.z+d; r12p3=a*x3+b*y3+c*z3+d; r23p=a2*P.x+b2*P.y+c2*P.z+d2; r23p1=a2*x1+b2*y1+c2*z1+d2; r13p=a1*P.x+b1*P.y+c1*P.z+d1; r13p2=a1*x2+b1*y2+c1*z2+d1; if(((r12p>=0 && r12p3>=0) || (r12p<=0 && r12p3<=0)) && ((r23p>=0 && r23p1>=0) || (r23p<=0 && r23p1<=0)) && ((r13p>=0 && r13p2>=0) || (r13p<=0 && r13p2<=0))) return true; else return false; } } 20 Il metodo bool proiezandappart(const Point & P) {}; calcola direttamente la proiezione di un punto 3D sul piano individuato da un triangolo e l’appartenenza della proiezione stessa a questo triangolo, inglobando i codici descritti nel paragrafo precedente ed in questo. Il metodo bool proiezandappart(const Point & P,double & dist) {}; restituisce anche la distanza fra punto 3D di partenza e sua proiezione. Infine il metodo bool proiezandappart(const Point & P,double & dist,const Point & N) {}; esegue gli stessi calcoli, però proiettando il punto P lungo la normale passata come input. 4 Ricerca del triangolo stl di appartenenza della proiezione di ogni nodo della mesh netgen Sfruttando le funzioni descritte nel paragrafo precedente è possibile raggiungere l’obiettivo prefissato per questo lavoro: per ogni nodo della mesh netgen si vuole calcolare la proiezione sulla superficie triangolata stl e trovare un triangolo stl a cui questa appartenga. Dato che può succedere di trovare più triangoli stl a cui appartiene la proiezione di un nodo netgen, si prenderà in considerazione il triangolo di appartenenza a minima distanza dal nodo netgen stesso. Per effettuare questa ricerca sono stati implementati tre metodi diversi, che verranno confrontati nel paragrafo successivo applicandoli ad un caso test e ad un caso reale. 4.1 Metodo brute force Questo metodo viene definito a forza bruta perchè usa due cicli for innestati, uno sui nodi netgen ed uno sui triangoli stl, e viene implementato per primo essendo il più immediato in quanto usa semplicemente il penultimo metodo introdotto nel paragrafo precedente, senza ulteriori calcoli. Il problema 21 collaterale è che la complessità computazionale di questo metodo crescerà quadraticamente all’aumentare dei nodi e dei triangoli delle mesh. Per ogni nodo netgen cicliamo, infatti, su tutti i triangoli stl, andando a calcolare la proiezione del nodo netgen sul piano individuato da ciascun triangolo e a verificare se essa appartiene al triangolo considerato di volta in volta. Fra tutti i triangoli stl a cui appartiene la proiezione del nodo netgen prendiamo quello a minima distanza e lo scriviamo in un file.txt e in un vettore. Otteniamo quindi il triangolo stl di appartenenza per ogni nodo del mesh netgen. Il codice implementato nel main è il seguente: vector<int> idtriangapp; idtriangapp.reserve(npMn); int idt(0); double dist; ofstream out("appBrutale.txt"); for(int pn=0;pn<npMn;++pn) //per ogni nodo della mesh netgen {cout<<"\n\npunto "<<pn<<" della mesh netgen"<<endl; idt=0; dis=0; dist=1000; for(int ts=0;ts<ntMs2;++ts) // per ogni triangolo della mesh stl {bool app=Ms2.triangolo(ts).proiezandappart(Mn.nodo(pn),dis); cout<<"il punto appartiene al triangolo? "<<app<<endl; cout<<"dis: "<<dis<<endl; if(app==1) { if(dis<dist) { idt=Ms2.triangolo(ts).getID(); dist=dis; } cout<<"id:"<<idt<<endl<<"dist: "<<dist<<endl; } } out<<"la proiezione a minima distanza del punto "<<Mn.nodo(pn)<< " della mesh netgen appartiene al "<<Ms2.triangolo(idt)<<" della mesh stl"<<endl; idtriangapp.push_back(idt); } 22 4.2 Metodo con ricerca del nodo stl più vicino al nodo netgen Questo metodo è meno immediato del primo perchè prevede il calcolo del patch di ogni nodo stl, cioè la creazione di una lista contenente gli ID di tutti i triangoli di cui il nodo stl è un vertice. Questi saranno i triangoli su cui cicleremo per trovare il triangolo stl di appartenenza della proiezione di ogni nodo netgen, velocizzando notevolmente la ricerca. Per ogni nodo netgen cicliamo su tutti i nodi stl per cercare quello a distanza minima. Questo doppio ciclo è molto più veloce di quello considerato nel metodo a forza bruta perchè serve per svolgere una semplice operazione di calcolo della distanza e non per la ricerca del triangolo di appartenenza. Questa viene eseguita solo sui triangoli appartenenti al patch del nodo stl più vicino ad ogni nodo netgen. Anche in questo caso consideriamo solo il triangolo stl di appartenenza a distanza minima. Riportiamo di seguito il codice scritto nel main: vector<list<int> > intorno; intorno=Ms2.intornopunto(); vector<int> idtriangapp1; idtriangapp1.reserve(npMn); ofstream patch("apppatch.txt"); double dismin; vector<double> distemp1; distemp1.reserve(npMs2); int idt1(0); double dis1; for(int pn=0; pn<npMn;++pn) {cout<<"\n\npunto "<<pn<<" della mesh netgen"<<endl; distemp1.clear(); idt1=0; dismin=1000; dis=0; dis1=1000; for(int ps=0; ps<npMs2; ++ps) { dis=distanza(Mn.nodo(pn),Ms2.nodo(ps)); cout<<"dis: "<<dis<<endl; distemp1.push_back(dis); } cout<<"tot distanze: "<<distemp1.size()<<endl; 23 for(int ps=0; ps<distemp1.size(); ++ps) { dismin=min(dismin,distemp1.at(ps)); } cout<<"distanza min: "<<dismin<<endl; int idtemp(0),idtemp1(0); for(int i=0; i<distemp1.size(); ++i) { if(distemp1.at(i)!=dismin) {++idtemp1; cout<<"idtemp1: "<<idtemp1<<endl;} else {idtemp=idtemp1; cout<<"idtemp: "<<idtemp<<endl;} } Point Pmindis=Ms2.nodo(idtemp); cout<<"punto a minima distanza: "<<Pmindis<<endl; list<int>::iterator p1=intorno.at(idtemp).begin(); dis=0; for(int j=0; j<intorno.at(idtemp).size();++j) { bool appart=Ms2.triangolo(*p1).proiezandappart(Mn.nodo(pn),dis); cout<<"il punto appartiene al triangolo? "<<appart<<endl; cout<<"dis: "<<dis<<endl; if(appart==1) { if(dis<dis1) { idt1=Ms2.triangolo(*p1).getID(); dis1=dis; } cout<<"id:"<<idt1<<endl<<"dis1: "<<dis1<<endl; } ++p1; } patch<<"la proiezione del punto "<<Mn.nodo(pn)<<" della mesh netgen appartiene al "<<Ms2.triangolo(idt1)<<" della mesh stl"<<endl; patch<<"punto a minima distanza: "<<Pmindis<<endl; idtriangapp1.push_back(idt1); } 24 4.3 Metodo con algoritmo di ricerca del nearest neighbor Questo metodo è ancora più avanzato e veloce del secondo perchè, al posto di ciclare su tutti i nodi stl per cercare quello più vicino ad ogni nodo netgen, usa un algoritmo di ricerca del nearest neighbor appartenente ad una libreria esterna specializzata nell’implementazione di questo codice: la libreria ANN. Questa libreria supporta la ricerca esatta e approssimata di k nearest neighbor, con k definito dall’utente e nel nostro caso = 1, in spazi di qualsiasi dimensione. Un set di punti dati, nel nostro caso il vettore di nodi della superficie stl, viene preprocessato per costruire una struttura dati, più precisamente un kd-tree, in modo che la ricerca del nearest neighbor per ogni nodo netgen sia più veloce ed efficiente. I kd-tree sono data structure basate su suddivisioni ortogonali ricorsive dello spazio in iperrettangoli disgiunti chiamati cells o boxes. Ogni nodo dell’albero è associato ad una di queste regioni ed al sottoinsieme di punti che giacciono in essa. La radice dell’albero è legata ad un box limitato che contiene tutti i punti dati, infatti è costituita da un puntatore al set di punti dati. Inoltre fornisce due ulteriori informazioni: la splitting rule ed una costante piccola chiamata bucket size. Queste informazioni sono necessarie per un nodo arbitrario dell’albero. Difatti, finchè il numero di punti associati a questo nodo è maggiore del bucket size il box viene suddiviso in due celle più piccole da un iperpiano con asse ortogonale che interseca il box stesso; la selezione dell’iperpiano dipende dalla regola di suddivisione considerata. I due box ottenuti vengono associati a due nodi figli del nodo in esame; i punti appartenenti al box originario sono suddivisi nelle due celle derivate a seconda del lato dell’iperpiano in cui si trovano, quelli che giacciono sull’iperpiano possono essere assegnati ad entrambi i box oppure vengono ridistribuiti secondo la splitting rule. Quando il numero di punti associati ad un nodo è minore del bucket size, questo nodo viene considerato una foglia e non viene più suddiviso. La struttura gerarchica dell’albero viene sfruttata nell’algoritmo di ricerca chiamato standard search, che si basa su un calcolo incrementale della distanza fra un nodo netgen e le celle associate ai nodi dell’albero. Questo algoritmo opera ricorsivamente: quando si incontra un nodo del kd-tree, l’algoritmo prima visita il figlio che è più vicino al punto netgen, in seguito, se l’altro figlio giace ad una distanza compresa fra 1 e 1 + ε volte la distanza dal più vicino punto trovato fino a quel momento, allora anche questo nodo viene visitato ricorsivamente. Una volta trovato il nearest neighbor di ogni nodo netgen si procede 25 come nel secondo metodo: si cerca il triangolo stl di appartenenza a minima distanza fra i triangoli appartenenti al patch del nearest neighbor calcolato. Il codice implementato nel main è il seguente: int k = 1; // number of nearest neighbors int dim = 3; // dimension double eps = 0; // error bound int m_pts = 1000000; // maximum number of data points int n_pts; // actual number of data points ANNpointArray data_pts; // data points ANNpoint query_pt; // query point ANNidxArray nn_idx; // near neighbor indices ANNdistArray dists; // near neighbor distances ANNkd_tree *the_tree; // search structure query_pt = annAllocPt(dim); // allocate query point data_pts = annAllocPts(m_pts, dim); // allocate data points nn_idx = new ANNidx[k]; // allocate near neigh indices dists = new ANNdist[k]; // allocate near neighbor dists n_pts = 0; // read data points for(int i=0; i<npMs2; ++i) { data_pts[i][0]=Ms2.nodo(i).x; data_pts[i][1]=Ms2.nodo(i).y; data_pts[i][2]=Ms2.nodo(i).z; n_pts++; } cout<<"numero data points: "<<n_pts<<endl; cout<<"Data Points:\n"; for(int j=0;j<n_pts;++j) { printPt(cout, data_pts[j]); } the_tree = new ANNkd_tree( // build search structure data_pts, // the data points n_pts, // number of points dim); // dimension of space for(int i=0; i<npMn; ++i) // read query points { 26 query_pt[0]=Mn.nodo(i).x; query_pt[1]=Mn.nodo(i).y; query_pt[2]=Mn.nodo(i).z; cout << "Query point: "; // echo query point printPt(cout, query_pt); the_tree->annkSearch( // search query_pt, // query point k, // number of near neighbors nn_idx, // nearest neighbors (returned) dists, // distance (returned) eps); // error bound std::ofstream ofile; ofile.open("nearestneig.txt",std::ios::app); for (int i = 0; i < k; i++) { // print summary printPt( ofile, data_pts[ nn_idx[i] ] ); }; ofile.close(); } //leggo il file nearestneig.txt e creo un vettore di punti //mettendo gli ID corretti Nodes vicini=creanearestneighbor("nearestneig.txt", Ms2.nodi()); //metodo che usa i nearest neighbor per trovare il triangolo della mesh stl //di appartenenza di ogni nodo della mesh netgen a minima distanza vector<int> idtriangapp2; idtriangapp2.reserve(npMn); int idt2(0); double dis2; ofstream patch1("apppatchnn.txt"); for(int k=0; k<vicini.size(); ++k) {idt2=-1; dis2=1000; dis=0; int id=vicini.at(k).ID; list<int>::iterator p2=intorno.at(id).begin(); for(int j=0; j<intorno.at(id).size();++j) { bool appart=Ms2.triangolo(*p2).proiezandappart(Mn.nodo(k),dis); 27 cout<<"il punto appartiene al triangolo? "<<appart<<endl; cout<<"dis: "<<dis<<endl; if(appart==1) { if(dis<dis2) { idt2=Ms2.triangolo(*p2).getID(); dis2=dis; } cout<<"id:"<<idt2<<endl<<"dis2: "<<dis2<<endl; } ++p2; } if(idt2!=-1) { patch1<<"la proiezione del punto "<<Mn.nodo(k)<<" della mesh netgen appartiene al "<<Ms2.triangolo(idt2)<<" della mesh stl"<<endl; } else { patch1<<"la proiezione del punto "<<Mn.nodo(k)<<" della mesh netgen non appartiene a nessun triangolo stl del patch del nearest neighbor"<<endl; } patch1<<"punto a minima distanza: "<<vicini.at(k)<<endl; idtriangapp2.push_back(idt2); } 5 Applicazioni e problemi riscontrati I tre metodi descritti nel paragrafo precedente sono stati applicati a due casi differenti. Il primo può essere considerato un caso test per verificare il corretto funzionamento del codice implementato, in quanto è costituito da una geometria molto semplice: un cilindro. Su questo cilindro sono state costruite due mesh: una più lasca (formata da 146 nodi) che sarà la mesh netgen presa in considerazione (vedere figura 7) ed una più fine (costituita da 846 nodi) che sarà, invece, la superficie triangolata stl (vedere figura 8). Facendo girare i tre codici su queste due mesh note si ottengono esattamente gli stessi risul- 28 Figura 7: Caso test del cilindro: mesh netgen. Figura 8: Caso test del cilindro: superficie stl. tati in tempi confrontabili e rapidi. Se si osservano bene i nearest neighbor dei nodi netgen si vede che hanno coordinate molto vicine ai nodi netgen stessi e si può notare che il triangolo stl di appartenenza correttamente fa parte del patch del nearest neighbor (vedere figura 9). Inoltre, considerando casualmente alcuni nodi netgen e visualizzandoli insieme alla loro proiezione e al triangolo stl di appartenenza calcolato (vedere figura 6), si può osservare che la proiezione correttamente appartiene al triangolo. Questo esempio ha quindi permesso di ottenere risultati sensati in accordo con l’implementazione del codice e di osservare che, per mesh molto piccole, il costo computazionale ed il tempo di calcolo dei tre metodi è all’incirca uguale. 29 Figura 9: Un nodo netgen con la sua proiezione, il suo nearest neighbor ed il patch di quest’ultimo. Il secondo caso, invece, è un caso reale: un arco aortico ricostruito a partire da immagini TAC. Anche in questo caso si dispone di una superficie triangolata stl, costituita però da tantissimi nodi (82674), e di una mesh netgen formata da 3107 nodi (vedere figura 10). Applicando i tre metodi si (a) mesh netgen (b) superficie stl Figura 10: Caso reale dell’aorta. 30 ottengono gli stessi risultati per il secondo ed il terzo, ma non per il primo, perchè in questo caso reale l’irregolarità della superficie comporta il fatto che alcuni nodi netgen abbiano una proiezione che non appartiene ad alcun triangolo del patch del nearest neighbor. Quindi, con il secondo e il terzo metodo non si riesce ad ottenere il triangolo stl di appartenenza per un numero elevato di nodi netgen; mentre con il metodo a forza bruta ciò succede solo per pochi nodi, dato che in questo metodo si considera un ciclo su tutti i triangoli stl. Perciò è necessario apportare dei cambiamenti che permettano di trovare un triangolo stl di appartenenza anche per questi nodi netgen. La prima idea è stata quella di calcolare una nuova normale, media delle normali ai triangoli appartenenti al patch dei nearest neighbor di questi nodi, e di proiettare il nodo netgen lungo questa normale per vedere se la nuova proiezione appartenga a uno dei triangoli del patch oppure no. Purtroppo questo cambiamento apporta pochissimi miglioramenti e permette di trovare il triangolo stl di appartenenza solo per un numero molto ridotto di nodi netgen. Si è quindi deciso di implementare due algoritmi che abbandonano l’idea della ricerca del triangolo di appartenenza e cercano i tre nodi stl più vicini ad ogni nodo netgen in maniera alternativa. Questi due algoritmi sono descritti nel paragrafo successivo. In questo caso reale si può osservare che il costo computazionale, il tempo di calcolo e l’efficienza variano notevolmente nei tre metodi per mesh con numero di nodi molto elevato. Il primo metodo, infatti, impiega 30 ore per il calcolo, il secondo 3 ore e 20 minuti, mentre il terzo pochi minuti. Per il vaso aortico, inoltre, abbiamo a disposizione anche gli spostamenti dei nodi stl in dieci tempi differenti. Perciò è stato possibile implementare il codice che calcola, una volta noti i tre punti stl più vicini ad ogni nodo netgen, l’interpolazione degli spostamenti di questi tre nodi e fornisce lo spostamento di ogni nodo netgen per tutti i tempi, permettendo di ricostruire la mesh netgen necessaria per i successivi calcoli fluidodinamici. 6 Algoritmi alternativi Per risolvere il problema riscontrato nell’applicazione del codice implementato al caso reale dell’aorta, sono stati scritti due algoritmi alternativi che calcolano i tre nodi stl più vicini ad ogni nodo netgen, non con la ricerca del triangolo stl di appartenenza della proiezione, ma in modo differente. Il primo algoritmo sfrutta la libreria ANN per la ricerca diretta dei tre 31 nearest neighbor. Come è già stato detto, infatti, questa libreria permette di calcolare in modo esatto ed approssimato i primi k nearest neighbor. Finora questa costante era stata posta = 1, per implementare il nuovo codice basta settare k = 3 ed i tre punti stl più vicini ad ogni nodo netgen vengono calcolati automaticamente. Il codice implementato per questo algoritmo è il seguente: int k = 3; // number of nearest neighbors int dim = 3; // dimension double eps = 0; // error bound int m_pts = 1000000; // maximum number of data points int n_pts; // actual number of data points ANNpointArray data_pts; // data points ANNpoint query_pt; // query point ANNidxArray nn_idx; // near neighbor indices ANNdistArray dists; // near neighbor distances ANNkd_tree *the_tree; // search structure query_pt = annAllocPt(dim); // allocate query point data_pts = annAllocPts(m_pts, dim); // allocate data points nn_idx = new ANNidx[k]; // allocate near neigh indices dists = new ANNdist[k]; // allocate near neighbor dists n_pts = 0; // read data points for(int i=0; i<noditriang.size(); ++i) { data_pts[i][0]=noditriang.at(i).x; data_pts[i][1]=noditriang.at(i).y; data_pts[i][2]=noditriang.at(i).z; n_pts++; } cout<<"numero data points: "<<n_pts<<endl; cout<<"Data Points:\n"; for(int j=0;j<n_pts;++j) { printPt(cout, data_pts[j]); } the_tree = new ANNkd_tree( // build search structure data_pts, // the data points n_pts, // number of points 32 dim); // dimension of space for(int i=0; i<npMn; ++i) // read query points { query_pt[0]=Mn.nodo(i).x; query_pt[1]=Mn.nodo(i).y; query_pt[2]=Mn.nodo(i).z; cout << "Query point: "; // echo query point printPt(cout, query_pt); the_tree->annkSearch( // search query_pt, // query point k, // number of near neighbors nn_idx, // nearest neighbors (returned) dists, // distance (returned) eps); // error bound std::ofstream ofile; ofile.open("nearestneig3.txt",std::ios::app); for (int i = 0; i < k; i++) { // print summary printPt( ofile, data_pts[ nn_idx[i] ] ); }; ofile.close(); } //leggo il file nearestneig3.txt e creo un vettore di punti //mettendo gli ID corretti Nodes vicini=creanearestneighbor("nearestneig3.txt", Ms2.nodi()); Il secondo algoritmo, invece, calcola il baricentro di ogni triangolo stl sfruttando il metodo apposito della classe Triangle. Successivamente effettua la ricerca del baricentro più vicino ad ogni nodo netgen, sempre usando la ricerca del nearest neighbor della libreria ANN. Infine considera come tre punti stl più vicini ad ogni nodo netgen i vertici del triangolo col baricentro più vicino. Riportiamo di seguito il codice che implementa questo algoritmo: //creo il vettore dei baricentri per la ricerca di quello + vicino //ad ogni nodo della mesh netgen Nodes baricentritriang; baricentritriang.reserve(ntMs2); for(int i=0;i<ntMs2;++i) {Point B; 33 B=Ms2.triangolo(i).baricentro(); B.ID=Ms2.triangolo(i).getID();//B.ID=i; baricentritriang.push_back(B); } int k = 1; // number of nearest neighbors int dim = 3; // dimension double eps = 0; // error bound int m_pts = 1000000; // maximum number of data points int n_pts; // actual number of data points ANNpointArray data_pts; // data points ANNpoint query_pt; // query point ANNidxArray nn_idx; // near neighbor indices ANNdistArray dists; // near neighbor distances ANNkd_tree *the_tree; // search structure query_pt = annAllocPt(dim); // allocate query point data_pts = annAllocPts(m_pts, dim); // allocate data points nn_idx = new ANNidx[k]; // allocate near neigh indices dists = new ANNdist[k]; // allocate near neighbor dists n_pts = 0; // read data points for(int i=0; i<baricentritriang.size(); ++i) { data_pts[i][0]=baricentritriang.at(i).x; data_pts[i][1]=baricentritriang.at(i).y; data_pts[i][2]=baricentritriang.at(i).z; n_pts++; } cout<<"numero data points: "<<n_pts<<endl; cout<<"Data Points:\n"; for(int j=0;j<n_pts;++j) { printPt(cout, data_pts[j]); } the_tree = new ANNkd_tree( // build search structure data_pts, // the data points n_pts, // number of points dim); // dimension of space for(int i=0; i<npMn; ++i) // read query points 34 { query_pt[0]=Mn.nodo(i).x; query_pt[1]=Mn.nodo(i).y; query_pt[2]=Mn.nodo(i).z; cout << "Query point: "; // echo query point printPt(cout, query_pt); the_tree->annkSearch( // search query_pt, // query point k, // number of near neighbors nn_idx, // nearest neighbors (returned) dists, // distance (returned) eps); // error bound std::ofstream ofile; ofile.open("nearestbar.txt",std::ios::app); for (int i = 0; i < k; i++) { // print summary printPt( ofile, data_pts[ nn_idx[i] ] ); }; ofile.close(); } Questi due algoritmi permettono di trovare i tre nodi stl più vicini ad ogni nodo netgen e quindi di calcolare il suo spostamento. Confrontando gli spostamenti dei nodi netgen ottenuti a partire da questi due codici, si può osservare che sono molto simili e per tantissimi nodi addirittura uguali. Perciò, a questo punto, è possibile ricostruire la mesh netgen superficiale ad ogni istante temporale, estenderla con l’harmonic extension a mesh volumetrica e procedere coi calcoli fluidodinamici. Riferimenti bibliografici [1] Daoqi Yang. “C++ AND OBJECT-ORIENTED NUMERIC COMPUTING for Scientists and Engineers”. Springer. [2] David M. Mount. “ANN Programming Manual ”. University of Maryland, College Park, Maryland 1998. [3] Renato Betti. “Lezioni di Geometria”. Masson, Milano 1996. [4] Wikipedia, the free encyclopedia: http://en.wikipedia.org 35