Chalmers


Datorstudioövning 7: System av PDE

Vi skall idag betrakta system av kopplade reaktions-diffusionsekvationer på formen:


        du_1/dt - div(a_1*grad(u_1)) = f_1,
  (1)   du_2/dt - div(a_2*grad(u_2)) = f_2,   i Omega,   t > 0,
        ...
        du_m/dt - div(a_m*grad(u_m)) = f_m,


    
          -a_1*Du_1/Dn = gamma_1*(u_1 - g_D_1) + g_N_1,
  (2)     -a_2*Du_2/Dn = gamma_2*(u_2 - g_D_2) + g_N_2,
          ...
          -a_m*Du_m/Dn = gamma_m*(u_m - g_D_m) + g_N_m,

            på randen till Omega,   t > 0,



              u_1(x,y,0) = u0_1(x,y),
  (3)         u_2(x,y,0) = u0_2(x,y),   i Omega,
              ...
              u_m(x,y,0) = u0_m(x,y).

Notera att vi måste specificera randvillkor (2) och begynnelsevillkor (3) till var och en av ekvationerna i systemet (1), samt att källtermen i ekvation nummer i, f_i = f_i(x, y, t, u) = f_i(x, y, t, u_1, u_2, ..., u_m), nu alltså (potentiellt) beror av samtliga komponenter av lösningen u = u(x, y, t) = [u_1(x, y, t); u_2(x, y, t); ...; u_m(x, y, t)]. Vi betraktar till att börja med följande exempel med två kopplade ekvationer:

        du_1/dt - (2/pi^2)*div(grad(u_1)) = u_1 + u_2,
  (4)   du_2/dt - (1/pi^2)*div(grad(u_2)) = 0.5*u_2 - u_1,

         i Omega = [0, 1] x [0, 1],   t > 0,


    
           u_1 = u_2 = 0,           0 < x < 1;   y = 1;   t > 0,
                                    x = 0;   0 < y < 1;   t > 0,
  (5)
           Du_1/Dn = Du_2/Dn = 0,   0 < x < 1;   y = 0;   t > 0,
                                    x = 1;   0 < y < 1;   t > 0,



  (6)         u_1(x,y,0) = 0,
              u_2(x,y,0) = sin(pi*x/2)*cos(pi*y/2),   i Omega.

Den exakta lösningen till (4)-(6) är:

        u_1(x,y,t) = sin(t)*sin(pi*x/2)*cos(pi*y/2),
  (7)
        u_2(x,y,t) = cos(t)*sin(pi*x/2)*cos(pi*y/2).

Verifiera att (7) är en lösning, inklusive randvillkoren (5) och begynnelsevillkoren (6)!

Kommentar: Även om vi kommer att ha detta specialfall med två ekvationer som ledstjärna när vi skriver vårt program, kommer det att fungera precis lika bra för det allmänna fallet med m stycken ekvationer!

Assemblerare: Våra assemblerare kommer nu att få lite mer att stå i! Styvhetsmatrisassembleraren kommer att få assemblera m stycken styvhetsmatriser (en för varje ekvation), lastvektorassembleraren kommer att få assemblera m stycken lastvektorer, och så vidare... Vi går nu igenom våra assemblerare en i taget och ser vad vi måste förändra:

1. Lastvektorassemblerare. Tanken är nu som sagt att lastvektorassembleraren skall assemblera m stycken lastvektorer, en för varje ekvation. (I exemplet (4)-(6) är alltså m = 2.) För att realisera detta måste vi först modifiera funktionen f_u_2D.m, som lastvektorassembleraren anropar vid beräkning av funktionsvärden till källtermen. För systemet (4) skulle vi t.ex. kunna skriva en funktionsfil, f_sys.m, enligt:


  function source = f_sys(x, y, t, u, delomradesnummer)

  % Inargument:
  %
  %   u:      1 x m - matris, där element (1, i) innehåller
  %                           lösningskomponent u_i(x, y, t)

  % Utargument:
  %
  %   source: 1 x m - matris, där element (1, i) innehåller
  %                           källtermskomponent f_i(x, y, t, u)
  
  source = zeros(1, 2);          % Initiera 1 x 2 - matris.

  source(1) = u(1) + u(2);       % f_1(x,y,t)
  source(2) = 0.5*u(2) - u(1);   % f_2(x,y,t)

Tänka noga igenom denna funktion: den returnerar alltså en radvektor innehållande [f_1 f_2 ... f_m]. Notera också att den förväntar sig att inargumentet u likaledes är en radvektor innehållande [u_1 u_2 ... u_m]. Detta måste vi förstås ha i åtanke när vi skriver vår lastvektorassemblerare.

Vi skriver nu en modifierad version, minLastAssemblerareUSys.m, av vår "skalära" lastvektorassemblerare. Tanken är att den skall returnera en nnodes x antal_ekv-matris, F, där kolonn nummer j innehåller lastvektorn för ekvation nummer j. (Vi använder fortsättningsvis variabelnamnet antal_ekv istället för m för att beteckna antalet ekvationer.) I stora drag skulle den kunna se ut så här:


  function F = minLastAssemblerareUSys(p, t, tid, U, antal_ekv);

  % U: nnodes x antal_ekv - matris innehållande (nodvärdena av)
  %    den approximativa lösningens samtliga komponenter vid
  %    tiden 'tid'. Nodvärdena för komponent u_j skall ligga
  %    i kolonn nummer j av U.

  nnodes = size(p,2);     % Antal noder. (= antal kolonner i p)
  ntri = size(t,2);       % Antal trianglar. (= antal kolonner i t)

  F = zeros(nnodes, antal_ekv);   % Initiering av lastvektorer.

  for n = 1:ntri          % loopa över samtliga trianglar

  % Ta reda på hörnens nodnummer:

  ... % som tidigare!

  % Räkna ut U:s värden i noderna i, j, och k:

    Unodi = U(i,:);   % 1 x antal_ekv - matris
    Unodj = U(j,:);   % 1 x antal_ekv - matris
    Unodk = U(k,:);   % 1 x antal_ekv - matris

  % Räkna ut källtermerna i triangelns hörn vid tiden "tid".
  % Notera att fnodi, fnodj, och fnodk blir 1 x antal_ekv - matriser:
  
    fnodi = f_sys(xnodi, ynodi, tid, Unodi, delomradesnummer);
    fnodj = f_sys(xnodj, ynodj, tid, Unodj, delomradesnummer);
    fnodk = f_sys(xnodk, ynodk, tid, Unodk, delomradesnummer);
  
  % Räkna ut värdena på phi_i, phi_j, och phi_k, i triangelns hörn:

  ... % som tidigare!

  % Hörnkvadratur:

    F(i,:) = F(i,:) + ...
        (fnodi*phi_inodi + fnodj*phi_inodj + fnodk*phi_inodk)*triangelarea/3;
    F(j,:) = F(j,:) + ...
        (fnodi*phi_jnodi + fnodj*phi_jnodj + fnodk*phi_jnodk)*triangelarea/3;
    F(k,:) = F(k,:) + ...
        (fnodi*phi_knodi + fnodj*phi_knodj + fnodk*phi_knodk)*triangelarea/3;

  end

Tänk mycket noga igenom hur detta går till. Koden är som du märker nästan identisk med tidigare, men eftersom fnodi, fnodj, och fnodk nu är (rad)vektorer assembleras samtliga ekvationer "på en gång". (Notera att phi_inodi, ..., och triangelarea fortfarande är skalärer, men att multiplicera en vektor med en skalär betyder ju att multiplicera varje komponent i vektorn med skalären.) Om du är osäker på denna matrissyntax i Matlab så experimentera lite direkt vid Matlab-prompten, t.ex.:

  >> A_test = [1 2; 3 4; 5 6]
  >> A_test(2,:) = A_test(2,:) + [2 4]*3

2. Randvektorassemblerare. När du tycker att du förstår hur lastvektorassembleraren fungerar, kan du på egen hand försöka modifiera randvektorassembleraren, till t.ex., minRandVekAssemblerareSys.m. Du måste förstås också modifiera randdatafunktionerna så att de passar till randvillkoren (5). För detta ändamål behöver du känna till numren på de fyra randsegmenten för enhetskvadraten på den triangulering vi senare skall räkna på. Du kan anta att sidan y = 0 har randsegmentnummer 1, och att de därefter är numrerade 2, 3, 4, i moturs ordning. Tips: Funktionsfilen som specificerar permeabiliteten, gamma_sys.m, skulle kunna se ut så här:


  function permeabilitet = gamma_sys(x,y,t,rand_nr)

  % Funktionen skall returnera en 1 x antal_ekv - matris,
  % där element (1, i) innehåller gamma_i.

  permeabilitet = zeros(1, 2);   % Initiera matris.

  if ((rand_nr == 3) | (rand_nr == 4))

    permeabilitet(1) = 1e5;     % gamma_1(x,y,t)
    permeabilitet(2) = 1e5;     % gamma_2(x,y,t)

  end

Randvektorassembleraren, minRandVekAssemblerareSys.m, skulle kunna börja så här:

  function G = minRandVekAssemblerareSys(p, e, tid, antal_ekv);

  nnodes = size(p,2);      % Antal noder. (= antal kolonner i p)
  nedges = size(e,2);      % Antal triangelsidor som ligger på ett
                           % randsegment. (= antal kolonner i e)

  G = zeros(nnodes, antal_ekv);    % Initiering av randvektorer.

  for l = 1:nedges         % Loopa över triangelsidor på
                           % randen till Omega.
  ... % osv!


OBS! I denna assemblerare förekommer produkter mellan gamma och g_D. Eftersom dessa nu är (rad)vektorer måste du här använda "point-wise"-operationen .*

3. Styvhetsmatrisassemblerare. Börja med att definiera funktionsfilen a_sys.m:


  function diffusion = a_sys(x, y, t, delomradesnummer)

  % Funktionen skall returnera en 1 x antal_ekv - matris,
  % där element (1, i) innehåller diffusionskoefficienten a_i

  diffusion = zeros(1, 2);   % Initiera matris.

  diffusion(1) = 2/(pi^2);    % a_1(x,y,t)
  diffusion(2) = 1/(pi^2);    % a_2(x,y,t)

När vi nu skall modifiera styvhetsmatrisassembleraren uppkommer ett problem: vi behöver nu skapa en styvhetsmatris för varje ekvation. Hur lagrar vi dessa? Jo, vi kan använda oss av Matlab:s tre-dimensionella matriser! I detta fall skapar vi en matris A av typen nnodes x nnodes x antal_ekv, där sedan styvhetsmatrisen för ekvation i fås som A(:,:,i) som är en "vanlig" nnodes x nnodes-matris. Experimentera lite:

  >> A_test = zeros(4,3,2)
  >> A_test(3,2,1) = 5
  >> A_test(:,3,2) = [1; 2; 3; 4]
  >> A_test(:,:,1)
  >> A_test(:,:,2)
  >> ...

Skapa nu t.ex. funktionsfilen minStyvhetsAssemblerareSys.m:

  function A = minStyvhetsAssemblerareSys(p, t, tid, antal_ekv);

  nnodes = size(p,2);          % Antal noder. (= antal kolonner i p)
  ntri = size(t,2);            % Antal trianglar. (= antal kolonner i t)

  A = zeros(nnodes, nnodes, antal_ekv);   % Initiering av styvhetsmatriser.

  for n = 1:ntri               % loopa över samtliga trianglar

  % Ta reda på hörnens nodnummer:

  ... % som tidigare!

  % Räkna ut diffusionskoefficienterna i triangelns hörn
  % vid tiden "tid". Notera att anodi, anodj, och anodk
  % är 1 x antal_ekv - matriser.
  
    anodi = a_sys(xnodi, ynodi, tid, delomradesnummer);
    anodj = a_sys(xnodj, ynodj, tid, delomradesnummer);
    anodk = a_sys(xnodk, ynodk, tid, delomradesnummer);
  
  % Gör om anodi, anodj, och anodk till 1 x 1 x antal_ekv -
  % matriser; behövs nedan för att dimensionerna skall stämma
  % vid assembleringen. Testa direkt vid Matlab-prompten hur
  % detta kommando fungerar, t.ex.,
  %  >> a_test = [12 9]
  %  >> a_test = reshape(a_test, 1, 1, 2)

    anodi = reshape(anodi, 1, 1, antal_ekv);
    anodj = reshape(anodj, 1, 1, antal_ekv);
    anodk = reshape(anodk, 1, 1, antal_ekv);

  % Räkna ut (de konstanta) gradienterna av phi_i, phi_j,
  % och phi_k.

  ... % som tidigare!

    A(i,i,:) = A(i,i,:) + ...    % 1 x 1 x antal_ekv - matris!
        (anodi*dot(gradphi_i,gradphi_i) + ...
         anodj*dot(gradphi_i,gradphi_i) + ...
         anodk*dot(gradphi_i,gradphi_i))*triangelarea/3;

  ... % som tidigare!

  end


4. Randmatrisassemblerare. Försök göra denna assemblerare på egen hand. Börja något i stil med:


  function K = minRandMatAssemblerareSys(p, e, tid, antal_ekv);

  nnodes = size(p,2);    % Antal noder. (= antal kolonner i p)
  nedges = size(e,2);    % Antal triangelsidor som ligger på ett
                         % randsegment. (= antal kolonner i e)

  K = zeros(nnodes, nnodes, antal_ekv);   % Initiering av randmatriser.

  for l = 1:nedges       % Loopa över triangelsidor på
                         % randen till Omega.

  ... % osv.!

Tips: Glöm inte att göra "reshape" av gammanodi och gammanodj.

5. Tidsmassmatrisassemblerare. Du kan behålla din skalära assemblerare för tidsmassmatrisen, eftersom den blir densamma för samtliga ekvationer i (1). (Man kan förstås tänka sig exempel i vilka man har en koefficient, c_i, också framför du_i/dt i (1); i sådana fall måste även denna assemblerare modifieras.)

Lösare: Slutligen modifierar vi vår lösare, som du t.ex. kan kalla för MyNLSystemSolver.m:


  antal_ekv = 2;  % Antal ekvationer.

  T = pi/2;       % Sluttid.

  dt = T/35;      % Tidsstegets längd.

  k = 0:dt:T;

  nnodes = size(p,2);

  L = length(k) - 1;        % Antal tidssteg.

  U = zeros(nnodes, antal_ekv, L+1);  % Initiera matris där lösningen lagras.

  for i = 1:nnodes
    x = p(1,i);
    y = p(2,i);
    U(i, :, 1) = u0_sys(x,y);  % Lagra begynnelsevärden.
  end

  % Tidsoberoende matriser och vektorer:

  M = minTidsMassAssemblerare(p, t);
  A_stor = minStyvhetsAssemblerareSys(p, t, 0, antal_ekv);
  K_stor = minRandMatAssemblerareSys(p, e, 0, antal_ekv);
  G_stor = minRandVekAssemblerareSys(p, e, 0, antal_ekv);

  % Beräkna LU-faktoriseringar:

  Lower_stor = zeros(nnodes, nnodes, antal_ekv);  % Initiera
  Upper_stor = zeros(nnodes, nnodes, antal_ekv);  % Initiera

  for ekv = 1:antal_ekv

    A = A_stor(:,:,ekv);
    K = K_stor(:,:,ekv);

    A_tot = M + 0.5*dt*(A + K);
  
    [Lower, Upper] = lu(A_tot);
  
    Lower_stor(:,:,ekv) = Lower;
    Upper_stor(:,:,ekv) = Upper;
  
  end
  
  for n = 1:L  % Tidsloop.
  
    tidsintervall = n   % Skriver ut aktuellt tidsintervall.
                        % Kan vara bra att veta hur långt
                        % programmet hunnit!
  
    t_h = k(n+1);       % Höger ändpunkt av tidsintervallet.
    t_v = k(n);         % Vänster ändpunkt av tidsintervallet.
  
    U_old = U(:, :, n);  % Lösning vid tiden t = t_v.
                         % nnodes x antal_ekv - matris
      
  % Tids- och u-beroende matriser och vektorer:
  
    F_v_stor = minLastAssemblerareUSys(p, t, t_v, U_old, antal_ekv);
  
  % Fixpunktsiteration:
  
    U_fix = U_old;      % Startgissning.
  
    TOL_fix = dt^3;     % Tolerans för Crank-Nicolson.
  
    fel_fix = TOL_fix + 1;  % Bara så att villkoret i while-loopen
                            % nedan uppfylls första gången.
  
    while (fel_fix > TOL_fix)
                      
      F_h_stor = minLastAssemblerareUSys(p, t, t_h, U_fix, antal_ekv);
  
      U_fix_old = U_fix;  % Spar nuvarande "iterat" för att kunna
                          % uppskatta felet när vi beräknat nästa.
  
      for ekv = 1:antal_ekv
      
        A = A_stor(:,:,ekv);  K = K_stor(:,:,ekv);
        F_v = F_v_stor(:,ekv);  F_h = F_h_stor(:,ekv);
        G = G_stor(:,ekv);
  
        Lower = Lower_stor(:,:,ekv);  Upper = Upper_stor(:,:,ekv);

        hl = (M - 0.5*dt*(A + K))*U_old(:,ekv) + ...
            0.5*dt*(F_v + G + F_h + G); 
  
        y = Lower\hl;
        U_fix(:,ekv) = Upper\y;
  
      end
  
      fel_fix = 0;
                      
      for ekv = 1:antal_ekv
  
        fel_fix = max([fel_fix norm(U_fix(:,ekv) - U_fix_old(:,ekv))]);
                          
      end
    
      fel_fix   % Skriv ut uppskattning av fixpunktsfelet.
      
    end
  
    disp('fixpunktsiteration ok')

    U_new = U_fix;
          
    U(:, :, n+1) = U_new;  % Lösning vid tiden t = t_h.
      
  end

OBS! Du måste modifiera u0_2D.m till u0_sys.m:

  function u0 = u0_sys(x,y)

  % Funktionen skall returnera en 1 x antal_ekv - matris, där
  % element (1, i) innehåller begynnelsevärde för ekvation 'i'.

  u0 = zeros(1, 2);   % Initiera matris.

  u0(1) = 0;                         % u0_1(x,y)
  u0(2) = sin(pi*x/2)*cos(pi*y/2);   % u0_2(x,y)

Testexempel: Prova att lösa exemplet (4)-(6), t.o.m. sluttiden T = pi/2. Använd samma mesh som när du räknade på den bi-stabila ekvationen (hämta den med >> load bistab_a_0_01), och 35 tidssteg. Eftersom, se (7), u_1(x, y, 0) = u_2(x, y, pi/2) och u_1(x, y, pi/2) = u_2(x, y, 0), kan du t.ex. jämföra dessa:


  >> U1 = reshape(U(:,1,:), nnodes, L+1);
  >> U2 = reshape(U(:,2,:), nnodes, L+1);
  >> max(abs(U1(:, 1) - U2(:, L+1)))
  >> max(abs(U2(:, 1) - U1(:, L+1)))

Du kan förstås också visualisera U1 och U2 var för sig med filmtajm.m, och jämföra med (7). Filmtajmtips: Sätt lämpligt värde på tiderperbild (kanske 2 eller 3). Tänk på att filmtajm.m förväntar sig att "lösningsmatrisen" som skall visualiseras har ett visst namn: du får ändra på lämpligt sätt så att det stämmer!

Lycka till!!!

Last modified: Wed Sep 4 12:16:37 MET DST 2002