Jej pochodna jest oczywiście zadana macierzą następującej postaci:
Postępując zgodnie ze schematem, w naszym programie najpierw powinniśmy zdefiniować funkcję F oraz macierz pochodnej. Dla większej elegancji, kody źródłowe funkcji i jej pochodnej będziemy trzymać w osobnych plikach. Umówmy się więc, że plik uklad2x2.c będzie zawierać kod źródłowy programu głównego, tzn. funkcję main(), podczas gdy definicja procedury obliczającej F(X) znajdzie się w pliku fun2x2.c, a kod dla jakobianu F´(X) w pliku jac2x2.c. W kolejnych rozdziałach przyjrzymy się dokładnie zawartości tych plików.
fun2x2.c
#include "snes.h" /* plik z definicjami funkcji
i obiektow SNES;
zawiera także dyrektywę #include "sles.h" */
/******************************************************
Procedura obliczajaca wartosci
funkcji F
******************************************************/
int Funkcja(SNES MojSnes,Vec X,Vec F,void *Ctx)
{
int ierr;
Scalar *TablicaX, *TablicaF;
/* Pobierz wskazniki do tablicy z wspolrzednymi X i F */
ierr = VecGetArray(X, &TablicaX); CHKERRQ(ierr);
ierr = VecGetArray(F, &TablicaF); CHKERRQ(ierr);
/* Oblicz funkcje F */
TablicaF[0] = TablicaX[0]*TablicaX[0] + TablicaX[1]*TablicaX[1]
1.0;
TablicaF[1] = TablicaX[0] + TablicaX[1];
/* Zamknij dostep do wartosci wektorow */
ierr = VecRestoreArray(X, &TablicaX); CHKERRQ(ierr);
ierr = VecRestoreArray(F, &TablicaF); CHKERRQ(ierr);
return(0);
}
Aby SNES mógł z nią współpracować, procedura obliczająca F(X) musi być zadeklarowana w ściśle określony sposób: int Funkcja(SNES MojSnes,Vec X,Vec F,void *Ctx). Pierwszy argument, MojSnes, to obiekt SNES, do którego później przekażemy tę funkcję. W naszych przykładach nigdy nie będziemy z niego bezpośrednio korzystać, ale warto pamiętać, że jest to możliwe. Wektor X jest argumentem funkcji F, natomiast do wektora F będą wpisywane wartości F(X). Ostatni parametr, Ctx, to kontekst definiowany przez użytkownika. Może on zostać wykorzystany do przekazania z programu dodatkowych parametrów potrzebnych do wyliczenia F, podobnie jak to było w przypadku definiowania preconditionera powłokowego (patrz rozdział "Tworzenie własnych preconditionerów"). W naszym przykładzie nie korzystamy z Ctx, niemniej musimy go uwzględnić w liście parametrów procedury Funkcja(), bo takiego formatu procedury będzie oczekiwał SNES.
Sama definicja Funkcji() nie jest skomplikowana. Mając dany wektor X (o dwóch współrzędnych), najpierw przygotujemy się do pobrania z niego kolejnych współrzędnych:
ierr = VecGetArray(X, &TablicaX); CHKERRQ(ierr);
TablicaF[0] = TablicaX[0]*TablicaX[0] + TablicaX[1]*TablicaX[1] 1.0; TablicaF[1] = TablicaX[0] + TablicaX[1];implementując wzór
Na zakończenie musimy pamiętać, aby odblokować wektory X i F komendą VecRestoreArray(). Ta funkcja uaktualnia wektor po dokonanym dostępie do tablicy jego wartości .
jac2x2.c
/******************************************************
Definicja pochodnej F
******************************************************/
#include "snes.h"
int Jakobian(SNES MojSnes, Vec X, Mat *Jac,
Mat *Precond, MatStructure *flag, void *Ctx)
{
Scalar *x;
/* Wskaznik do wartosci wektora X */
Scalar A[4];
/* Tu wpiszemy elementy macierzy Jacobiego */
int ierr;
int idx[2] = {0,1};
ierr = VecGetArray(X, &x); CHKERRQ(ierr);
/* Oblicz elementy jakobianu i wstaw do macierzy Jakobian */
A[0] = 2.0*x[0]; A[1] = 2.0*x[1]; /* pierwszy wiersz */
A[2] = 1.0; A[3] = 1.0;
/* drugi wiersz */
ierr = MatSetValues(*Jac,2,idx,2,idx,A,INSERT_VALUES); CHKERRQ(ierr);
*flag = SAME_NONZERO_PATTERN;
/* Odblokuj i uaktualnij X */
ierr = VecRestoreArray(X,&x); CHKERRQ(ierr);
/* Zloz macierz */
ierr = MatAssemblyBegin(*Jac, MAT_FINAL_ASSEMBLY); CHKERRQ(ierr);
ierr = MatAssemblyEnd(*Jac, MAT_FINAL_ASSEMBLY); CHKERRQ(ierr);
return(0);
}
int Jakobian(SNES MojSnes, Vec X, Mat *Jac, Mat *Precond, MatStructure *flag, void *Ctx);
gdzie MojSnes jest obiektem SNES do którego zostanie później przekazana ta procedura, a X jest punktem w którym liczymy jakobian. Do macierzy Jac (zwróćmy uwagę, że argumentem jest wskaźnik, a nie sama macierz!) jest wpisywana wyznaczona macierz pochodnej i to jest podstawowe zadanie tej procedury: dostarczenie sposobu obliczania macierzy F'(X).
Macierz Precond oznacza macierz preconditionera (zazwyczaj będzie to ta sama macierz Jac). Parametr flag jest informacją dla PETSc, czy w następnej iteracji Newtona będzie można wykorzystać struktury preconditionera, zbudowane na użytek poprzedniej - podobnie jak w przypadku funkcji SLESSetOperators(). Wartości, jakie można przypisywać parametrowi flag były opisane w poprzedniej części (patrz "SLES, czyli rozwiązywanie równań liniowych"). Ostatni parametr, Ctx, to kontekst definiowany przez użytkownika. Może on zostać wykorzystany do przekazania dodatkowych parametrów z programu potrzebnych do wyliczenia F', podobnie jak w przypadku implementacji funkcji F w procedurze Funkcja(). W naszym przykładzie nie korzystamy z Ctx.
uklad2x2.c
#include "snes.h"
#include <stdio.h>
static char help[] = "Rozwiazuje nieliniowy uklad 2x2\n\
Opcje: -x0 [Scalar] -y0 [Scalar] wartosci wspolrzednych przybl. poczatk.\n\n";
/* Deklaracje procedur obliczajacych funkcje i jakobian */
int Jakobian(SNES,Vec,Mat*,Mat*,MatStructure*,void*);
int Funkcja(SNES,Vec,Vec,void*);
/******************************************************
Program glowny
******************************************************/
int main( int argc, char **argv )
{
SNES MojSnes;
/* obiekt SNES, ktorym rozwiazemy zadanie */
Vec X, R;
/* wektor rozwiazania i residualny */
Mat MatJ;
/* macierz jakobianu */
int ierr, flg, its;
int idx[2] = {0,1};
Scalar X0[2];
/* wspolrz. przyblizenia poczatkowego */
PetscInitialize( &argc, &argv,PETSC_NULL,help );
/* Tworzenie potrzebnych obiektow: SNESu, wektorow, macierzy jakobianu
*/
ierr = SNESCreate(MPI_COMM_WORLD,SNES_NONLINEAR_EQUATIONS,&MojSnes);
CHKERRA(ierr);
ierr = VecCreate(MPI_COMM_WORLD,PETSC_DECIDE,2,&X); CHKERRA(ierr);
ierr = VecDuplicate(X,&R); CHKERRA(ierr);
ierr = MatCreate(MPI_COMM_WORLD,2,2,&MatJ); CHKERRA(ierr);
/* Przekaz SNESowi funkcje i wektor residualny */
ierr = SNESSetFunction(MojSnes, R, Funkcja, PETSC_NULL); CHKERRA(ierr);
/* Przekaz SNESowi macierz jakobianu i funkcje obliczajaca jakobian
*/
ierr = SNESSetJacobian(MojSnes,MatJ,MatJ,Jakobian,PETSC_NULL);CHKERRA(ierr);
/* Uwzglednij opcje uruchomienia SNESu typu -snes_view -snes_monitor
*/
ierr = SNESSetFromOptions(MojSnes); CHKERRA(ierr);
/* Przyblizenie poczatkowe */
ierr = OptionsGetScalar( PETSC_NULL, "-x0", &X0[0], &flg
); CHKERRA(ierr);
if( !flg ) X0[0] = 1.0;
ierr = OptionsGetScalar( PETSC_NULL, "-y0", &X0[1], &flg
); CHKERRA(ierr);
if( !flg ) X0[1] = 2.0;
ierr = VecSetValues(X, 2, idx, X0, INSERT_VALUES ); CHKERRA(ierr);
ierr = VecAssemblyBegin(X); CHKERRA(ierr);
ierr = VecAssemblyEnd(X); CHKERRA(ierr);
/* Rozwiaz! */
ierr = SNESSolve(MojSnes,X,&its); CHKERRA(ierr);
PetscPrintf(MPI_COMM_WORLD, "Liczba iteracji = %d\n", its);
PetscPrintf(MPI_COMM_WORLD,"Wektor rozwiazania:\n");
ierr = VecView(X, VIEWER_STDOUT_WORLD); CHKERRA(ierr);
/* Zwolnij pamiec */
ierr = VecDestroy(X); CHKERRA(ierr);
ierr = VecDestroy(R); CHKERRA(ierr);
ierr = MatDestroy(MatJ); CHKERRA(ierr);
ierr = SNESDestroy(MojSnes); CHKERRA(ierr);
/* Koniec bajki */
PetscFinalize();
return 0;
}
Przyjrzyjmy się funkcji main(). Przeglądając pobieżnie komentarze, możemy stwierdzić z satysfakcją, że zapowiedź z rozdziału "Schemat uzycia SNESu do rozwiązania równania F(x*)=0 " spełniła się: w kolejnych krokach wykonywania programu
Oto przykładowy wynik uruchomienia powyższego programu:
11:/home/staff/przykry/Petsc> test Liczba iteracji = 7 Wektor rozwiazania: -0.707107 0.707107
#include "snes.h" #include <stdio.h>
static char help[] = "Rozwiazuje nieliniowy uklad 2x2\n\ Opcje: -x0 [Scalar] -y0 [Scalar] wartosci wspolrzednych przybl. poczatk.\n\n";
int Jakobian(SNES,Vec,Mat*,Mat*,MatStructure*,void*); int Funkcja(SNES,Vec,Vec,void*);
Na początku funkcji main() deklarujemy kilka zmiennych roboczych:
SNES MojSnes; /* obiekt SNES, ktorym rozwiazemy zadanie */ Vec X, R; /* wektor rozwiazania i residualny */ Mat MatJ; /* macierz jakobianu */ int ierr, flg, its; int idx[2] = {0,1}; Scalar X0[2]; /* wspolrz. przyblizenia poczatkowego */
PetscInitialize( &argc, &argv,PETSC_NULL,help );
SNESCreate(MPI_COMM_WORLD,SNES_NONLINEAR_EQUATIONS,&MojSnes);
VecCreate(MPI_COMM_WORLD,PETSC_DECIDE,2,&X); VecDuplicate(X,&R);
MatCreate(MPI_COMM_WORLD,2,2,&MatJ);
Druga funkcja przekazuje do SNESu macierz jakobianu i metodę uaktualniania go: int SNESSetJacobian( SNES MojSnes, Mat Jac, Mat Prec, int (*Jakobian)(SNES, Vec, Mat*, Mat*, MatStructure*,void*), void *Ctx ) Podobnie jak poprzednio, pierwszym argumentem jest MojSnes - obiekt SNESu, do którego chcemy przekazać funkcję obliczającą F'(X), tzn. macierz jakobianu. Drugim argumentem jest więc macierz jakobianu Jac, do której będą wprowadzane wartości elementów, obliczone w procedurze Jakobian(). Trzecim argumentem jest macierz preconditionera Prec dla macierzy Jac, przydatna przy rozwiązywaniu układów z tą macierzą. Zazwyczaj wystarcza podać jako Prec po prostu tą samą macierz Jac. Kolejny, czwarty argument to nazwa procedury - u nas: Jakobian() - obliczającej elementy macierzy jakobianu F'(X). Jak widać z powyższego formatu, SNES (i kompilator...) oczekuje, że funkcja Jakobian()jest deklarowana jako int Jakobian(SNES MojSnes,Mat *Jac,Mat *Prec,int *flg,void *Ctx) i właśnie dlatego w taki, a nie inny sposób definiowaliśmy naszą procedurę Jakobian() w pliku jac2x2.c powyżej. Ostatni argument dla SNESSetJacobian() to wskaźnik do kontekstu, wykorzystywanego przez procedurę Jakobian() i zawierającego dane używane przy wyznaczaniu macierzy F'(X).
Ponieważ nasze procedury: Funkcja()oraz Jakobian() nie korzystają z kontekstu użytkownika, jako ostatni argument wywołania SNESSetFunction() i SNESSetJacobian() podajemy PETSC_NULL:
/* Przekaz SNESowi funkcje i wektor residualny */ SNESSetFunction(MojSnes, R, Funkcja, PETSC_NULL); /* Przekaz SNESowi macierz jakobianu i funkcje obliczajaca jakobian */ SNESSetJacobian(MojSnes,MatJ,MatJ,Jakobian,PETSC_NULL); /* Uwzglednij opcje uruchomienia SNESu typu -snes_view -snes_monitor */ SNESSetFromOptions(MojSnes);
Wszystkie dane o równaniu są już przekazane do SNESu, opcje ustawione, moglibyśmy więc już rozwiązać równanie... jeśli tylko określilibyśmy przybliżenie początkowe. Przybliżenie początkowe (analogicznie jak w SLESie) wpiszemy wprost do wektora rozwiązania. Właśnie do tego przydadzą się nam tablice X0 i idx, zadeklarowane na początku programu.
/* Przyblizenie poczatkowe */ OptionsGetScalar( PETSC_NULL, "-x0", &X0[0], &flg ); if( !flg ) X0[0] = 1.0; OptionsGetScalar( PETSC_NULL, "-y0", &X0[1], &flg ); if( !flg ) X0[1] = 2.0; VecSetValues(X, 2, idx, X0, INSERT_VALUES ); ierr = VecAssemblyBegin(X); CHKERRA(ierr); ierr = VecAssemblyEnd(X); CHKERRA(ierr);
18:/home/staff/przykry/Petsc> test -y0 5.0
Po określeniu przybliżenia początkowego nie pozostaje nam już nic innego, jak nakazać SNESowi rozwiązanie naszego zadania:
SNESSolve(MojSnes,X,&its);
PetscPrintf(MPI_COMM_WORLD, "Liczba iteracji = %d\n", its); PetscPrintf(MPI_COMM_WORLD,"Wektor rozwiazania:\n"); VecView(X, VIEWER_STDOUT_WORLD);
VecDestroy(X); VecDestroy(R); MatDestroy(MatJ); SNESDestroy(MojSnes); PetscFinalize();
Makefile include $(PETSC_DIR)/bmake/$(PETSC_ARCH)/base CC = gcc -DPETSC_LOG CLINKER = gcc CFLAGS = $(PCONF) $(PETSC_INCLUDE) $(BS_INCLUDE) LIBBASE = libpetscsles libpetscsnes OPTS = -O BOPT = O OBJECTS_EX1 = uklad2x2.o fun2x2.o jac2x2.o EXECUTABLE = test ex1: $(OBJECTS_EX1) $(CLINKER) -o $(EXECUTABLE) $(OBJECTS_EX1) \ -lm $(PETSC_LIB) # # ex1 source files # uklad2x2.o: uklad2x2.c $(CC) $(CFLAGS) $(OPTS) -c uklad2x2.c fun2x2.o: fun2x2.c $(CC) $(CFLAGS) $(OPTS) -c fun2x2.c jac2x2.o: jac2x2.c $(CC) $(CFLAGS) $(OPTS) -c jac2x2.c