Polynomanpassningsprogram Den här uppgiften skall göra en polynomanpassning av en tvåkolumners tabell enligt minstakvadrat-kriteriet och presentera resultatet grafiskt. Uppgiften skall lösas både med skaläralgebra och matrisalgebra. För den som vill fördjupa sig i den bakomliggande matematiken hänvisar jag till boken "Numerical Recipes in C++" från Cambridge University Press,1988 eller till kompeniet "Felkalkyl" av Einar Lindholm, 1954. I ditt program skall man kunna välja datafil. Det gör man genom att lägga in klassen OpenDialog som ligger i klassbiblioteket Dialogs. Låt filtypen vara *.txt men möjliggör även att browsa efter filtypen *.* I uppgifterna kryptering och koppla till annat program... finns i detalj beskrivet hur man använder OpenDialogen. När man har valt datafil skall knappen Editera aktiveras. Genom att klicka på den knappen skall Window s inbyggda editor Notepad startas och den valda datafilen öppnas. Man skall då kunna skriva in sin tvåkolumners tabell och spara den. Vidare skall det finnas möjlighet att välja gradtalet hos polynomet. Det skall man välja med en ComboBox och valen skall vara heltalen från noll till sex. När fil och gradtal är valt skall knappen Calc Scalar och knappen "Calc Matrix" aktiveras. Man skall alltså kunna välja att göra beräkningarna med skalärmatematik eller matrismatematik. Klickar man på den skall programmet öppna datafilen och läsa in tvåkolumners tabellen och visa hur många datapunkter den innehöll. Därefter skall programmet göra en minstakvadratanpassning av talen till ett polynom av det valda gradtalet. De framräknade Per Nylén - 29 maj 2008 1(6)
koefficienterna i polynomet samt RMS (Root Mean Square) skall visas i ett RichEdit-fönster, som finns i klassbiblioteket Win32 respektive i ett Edit-fönster som finns i Standardbiblioteket. För att visa grafiskt hur anpassningen gick skall ditt program innehålla två diagram. Använd dig av klassen Chart som ligger i klassbiblioteket Additional. Denna klass har väldigt många egenskaper så för Chart kan man öppna en Objektinspektor genom att dubbelklicka i diagrammet. En kurva i ett diagram kallas för Series. Det vänstra diagrammet skall innehålla två Series: primärdata som skall visas som points och en line som visar det framräknade polynomet. Varje datapunkt i datafilen skall alltså visas som en röd rektangel och polynomet visas som en sammanhängande linje. I det högra diagrammet skall felen mellan datapunkterna och det värde som polynomet ger, visas i form av ett stapeldiagram, Bar. Sätt också ut titlar på diagrammen. Felen mellan datapunkterna och polynomet brukar kallas för residuals, dvs. restfel. Själva polynomanpassningen kräver en del räknande. Enklast uttrycker man det på matrisformalism. Vi ställer upp ekvationssystemet Y = X * A (1) där Y är vektorn y 1,y 2,y 3 osv. som bildas av y-värdena. A är den obekanta vektorn som bildas av koefficienterna A 0,A 1,A 2 osv i polynomet, och X är den matris som bildas av j x-värdenas potenser upp till polynomets gradtal, dvs: x ij =x i där i=1..n och j=0..p, där n är antalet datapunkter och p är polynomets gradtal. För att lösa ekv.1 förlänger man ekvation 1. med matrisen X s transponat ( X T ) och får då X T *Y = X T *X*A (2) Detta kallas för normalekvationerna och är p+1 ekvationer i p+1 obekanta. Den kan alltså ha en exakt lösning. Lösningen får man genom att förlänga med inversen till den kvadratiska matrisen X T *X, dvs: (X T *X) -1 *X T *Y = A (3) Har man tillgång till matrisoperationer behövs således bara lite matrismultiplikationer och en invertering, men då blir det ju inget att bita i. Man kan också lösa problemet med skalär algebra, enligt följande recept: Börja med att öppna datafilen och läsa in mätvärdena till två arrayer X och Y. ifstream f1(fil); i=0; while (!f1.eof() ) i=i+1; f1>>x[i]>>y[i]; Antal=i-1; //Läs in data från filen Per Nylén - 29 maj 2008 2(6)
där X och Y är två arrayer [1..50] av typen Extended. Jag har begränsat antalet datapar till 50. För att minimera avrundningsfelen i beräkningarna utnyttjar jag processorn maximalt genom att använda datatypen Extended som representerar ett tal på potensform i 80 bitar. När du är klar med inläsningen skall du stänga filen. Vi kommer att behöva veta intervallet hos X-värdena, därför bestämmer du X max och X min med satserna: Xmin=X[1]; Xmax=X[1]; for (j=1; j<=antal; j=j+1) if (Xmin>X[j]) Xmin=X[j];; if (Xmax<X[j]) Xmax=X[j];; //Bestäm Xmin och Xmax Nu är det dags att formulera normalekvationerna. Det gör du med satserna: Gradtal := boxorder.itemindex; //Hämta gradtalet for (j=0; j<=gradtal; j=j+1) //Sätt upp normalekvationerna for (i=1; i<=antal; i=i+1) Z[i]=Y[i]*Power(X[i],j); for(k=0; k<=gradtal; k=k+1)b[i][k]=power(x[i],j+k); R[j]=0; for(i=1; i<=antal; i=i+1) R[j]=R[j]+Z[i];; for(i=0; i<=gradtal; i=i+1) M[j][i]=0;; for(i=0; i<=gradtal; i=i+1) for(k=1; k<=antal; k=k+1)m[j][i]=m[j][i]+b[k][i]; ; ; där Z är en array [1..50] av Extended, B är en array [1..50, 0..6] av Extended, R är en array [0..6] av Extended och slutligen M är en array [0..6, 0..6] av typen Extended som innehåller koefficienterna i normalekvationerna. Slutligen skall vi lösa normalekvationerna och beräkna koefficienterna. Det gör vi med s.k. Gauss-Jordan elimination och återsubstitution. for (i=0; i<gradtal; i=i+1) //Gausselimination for (j=i+1; j<=gradtal; j=j+1) F=M[j][i] / M[i][i]; R[j]=R[j]-R[i]*F; for(k=i; k<=gradtal; k=k+1)m[j][k]=m[j][k]-m[i][k]*f;; ; ; for (i=0; i<=6; i=i+1) A[i]=0;; for (i=gradtal; i>=0; i=i-1) A[i]=R[i]/M[i][i]; for(j=i-1; j>=0; j=j-1)r[j]=r[j]-m[j][i]*a[i];; ; //Beräkna koefficienterna Per Nylén - 29 maj 2008 3(6)
Vi har nu koefficienterna liggande i A[0] till A[Gradtal] och kan skriva ut dem i Koefficientfönstret. Jag använder mig av klassen RichEdit som ligger i klassbiblioteket Win32. För att få utskriften snygg bör du använda funktionen FloatToStrF som ger möjlighet till formatterad utskrift. Dessutom kollar jag om talet är positivt eller negativt. reresult->text=""; for (i=0; i<=gradtal; i=i+1) if(a[i]<0)taltext=": " +FloatToStrF(A[i],ffExponent,4,2); else TalText=": "+FloatToStrF(A[i],ffExponent,4,2); reresult->text=reresult->text+" "+IntToStr(i)+TalText+'\r'+'\n'; Nu skall du också presentera resultatet i de två diagrammen. I det vänstra diagrammet visas primärdata som punkter och det återberäknade polynomet som en kurva. Det vänstra diagrammet innehåller alltså två Series, där det första är av typen point och det andra av typen line. För att rita en ny kurva i ett diagram, börjar du med att radera den gamla med satsen Series1->Clear(); Därefter ritar du kurvan genom att anropa seriens metod AddXY for (i=1; i<=antal; i=i+1)series1->addxy(x[i],y[i],"",clred);; //Rita primärdata På liknande sätt ritar du det återberäknade polynomet som en linje. För att få en snygg kurva har jag valt att rita kurvan med 10 gånger fler punkter än mina primärdata, och jag ritar den från X min till X max, som jag har beräknat tidigare. for (i=0; i<=antal*10; i=i+1) XX=Xmin+(Xmax-Xmin)*i/(Antal*10); Series2->AddXY(XX,Poly(XX,&A[0],Gradtal),"",clNavy); För att beräkna punkter på kurvan använder jag mig av funktionen Poly(...). För mina matteoperationer inkluderar jag därför filerna <math.hpp> och <math.h>. För att kunna studera detaljer i det här diagrammet vill jag att man skall kunna zooma in ett område och kunna gå tillbaka till den ursprungliga grafen. Zoomningen ställer du in i diagrammets General-flik, och för att gå tillbaka till den ursprungliga grafen när man klickar i grafen anropar man metoden: Chart1.UndoZoom(); I det högra diagrammet skall du visa felet i varje mätpunkt, dvs skillnaden mellan primärdatas Y-värde och det värde som polynomet ger om man sätter in motsvarande X-värde. Samtidigt som du räknar ut felen (residualerna) passar du på att beräkna felkvadratsumman och därur RMS (Root Mean Square). Per Nylén - 29 maj 2008 4(6)
RMS=0; for(i=1; i<=antal; i=i+1) Err[i]=Y[i]-Poly(X[i],&A[0],Gradtal); RMS=RMS+Err[i]*Err[i]; if(antal>gradtal) RMS=sqrt(RMS/(Antal-Gradtal)); else RMS=0;; Om ekvationssystemet inte är överbestämt utan antalet ekvationer är lika med antalet obekanta får jag en exakt lösning och RMS=0. Jag skulle då dividera med noll när jag beräknar RMS. Därför gör jag en särskild koll av det. De beräknade residualerna skall jag nu visa som ett stapeldiagram (bar graph) i det högra diagrammet. Series3->Clear(); for(i=1; i<=antal; i=i+1)series3->addxy(x[i],err[i],"",clred);; Slutligen kan jag skriva ut värdet på RMS och antalet datapunkter i respektive fönster: edrms->text=" "+FloatToStrF(RMS,ffExponent,4,2); edantal->text=inttostr(antal); Avsluta med att provköra programmet på en känd datamängd, t.ex. heltalen och deras kvadrater. Felsökning. När man skriver ett program med så många nestlade satser och indices som går i varandra, är det lätt att det insmyger sig ett litet fel. För att underlätta eventuell felsökning kan du köra programmet stegvis med hjälp av brytpunkter. Här ger jag några mellanresultat som kan underlätta felsökningen: Primärdata är en tabell med tre datapar som anpassas till ett andragradspolynom: Primärdata Normalekvationerna M M efter Gausselimination Resultatet A 1 4 3 6 14 3 6 14 3 0 1 2 7 6 14 36 0 2 8 3 12 14 36 98 0 0 0,67 Per Nylén - 29 maj 2008 5(6)
Matrisalgebra. Du skall även kunna utföra beräkningarna med matrisalgebra. För att kunna räkna med matriser, som ju inte är en standardtyp i C++, lägger du till en ny klass till ditt program. Det gör du genom att lägga till satserna #include "matrix.h" och using namespace math; i.h-filen i ditt program. Filen matrix.h han du hämta från nätet vid www.techsoftpl.com/matrix/download.htm eller från min hemsida. Du får då en zip-fil (Lite-versionen) som du måste packa upp. Gör det och där skall du finna filen "matrix.h" som du lägger in i ditt projekts katalog. För att deklarera en matris med en viss storlek och av en viss datatyp skriver man t.ex: matrix<extended> x(antal,gradtal+1); matrix<extended> xt(gradtal+1,antal); matrix<extended> y(antal,1); matrix<extended> a(antal,1); // x-matrisen // x-transponat // y-värdena // resulterande koefficienter dvs samma syntax som när man deklarerar en vector. Du får då en matris (a) med element av typen "Extended" som är "Antal" rader och 1 kolumn stor. Indexeringen i matrisen börjar givetvis vid 0,0. Förutom de vanliga matrisoperationerna ( + * osv ) kan man transponera en matris med tildeoperatorn (~) och invertera en matris med operatorn.inv(). Ett element i matrisen kommer man åt med t.ex. x(i,j) Observera de runda parenteserna som man använder när man indexerar i en vector eller matris. Inläsning av data från filen och utskriften av resultaten till graferna går givetvis till på samma sätt som när man utför räkningarna med skaläralgebra. Fyll matrisen y med data från arrayen Y med repetitionssatsen: for(i=0; i<antal; i++) y(i,0)=y[i]; ; och sätt upp matrisen x med satserna: for (i=0; i<antal; i=i+1) for (j=0; j<=gradtal; j=j+1) x(i,j)=pow(x[i],j); ; Du kan nu beräkta transponatet xt av x med satsen xt=~x; För att bestämma koefficienterna ur ekv.3 A =(X T *X) -1 *X T *Y skriver vi alltså a = (xt*x).inv() * xt*y; och de resulterande koefficienterna som ligger i matrisen a kopierar du över till arrayen A: for(i=0; i<=gradtal; i++) A[i]=a(i,0); ; Du har nu de resulterande koefficienterna liggande i A och kan generera graferna på samma sätt som vid skaläralgebra. Per Nylén - 18 maj 2001 6(6)