Ohjelmointi ja numeeriset menetelmät, luento 8, 15.3.


Geneeriset proseduurit

Esimerkiksi max on geneerinen proseduuri. Todellisten argumenttien tyypeistä riippuen se voi palauttaa kokonaisluvun, reaaliluvun tai kaksoistarkkuuden luvun.

Geneerinen proseduuri on yhteinen nimi joukolle eri proseduureja, joista valitaan suoritettavaksi yksi todellisten argumenttien tyyppien perusteella.

Eri proseduurien on erottava toisistaan argumenttien tyyppien suhteen, jotta todellisten argumenttien perusteella voidaan päättää, mikä proseduuri suoritetaan. Pelkkä argumenttien järjestyksen vaihtaminen ei riitä (koska voidaan kutsua avainsana-argumenttien avulla).

        module div2
        private
        interface half
           module procedure half1, half2
        end interface
        public :: half

        contains

        integer function half1(i)
          integer, intent(in) :: i
          half1 = i/2
        end function

        real function half2(x)
          real, intent(in) :: x
          half2 = x/2.0
        end function
        end module
Proseduurin nimi half viittaa kahteen eri proseduuriin, joista suoritetaan jompikumpi. Tässä half1 ja half2 ovat yksityisiä, joten niitä ei voi kutsua suoraan.
        program divtest
          use div2
          integer :: i=10
          real :: x=1.0
          i=half(i)
          x=half(x)
          write(*,*) i,x
       end program


Operaattorien ylilataus (overloading)

Periaatteessa operaatttori on yhden tai kahden muuttujan funktio, joka voi myös olla geneerinen. Operaattorien tapauksessa argumenttien järjestyksellä on merkitystä.

Esimerkiksi yhteenlasku: määritellään yhteenlaskun suorittava funktio erikseen kullekin mahdolliselle tyyppikombinaatiolle.

        interface operator (+)
            module procedure ratint_add, intrat_add, add
        end interface

        ...
        contains
        ...

        function add (p, q)
        ! lasketaan rationaalilukujen p ja q summa
          type (rational) :: add
          type (rational), intent(in):: p, q
          ...
        end function

        function ratint_add (p, i)
        ! lasketaan kokonaisluvun i ja rationaaliluvun p summa
          type (rational) :: ratint_add
          type (rational), intent(in):: p
          integer, intent(in) :: i
          integer a,b,c,d
          type (rational) :: r
          a=p%nominator; b=p%denominator
          c=i; d=1
          r%nominator = a*d + b*c
          r%denominator = b*d
          ratint_add = simplify (r)
       end function

       function intrat_add (i, q)
       ! lasketaan kokonaisluvun i ja rationaaliluvun q summa
         type (rational) :: intrat_add
         integer, intent(in) :: i
         type (rational), intent(in):: q
         integer a,b,c,d
         type (rational) :: r
         a=i; b=1
         c=q%nominator; d=q%denominator
         r%nominator = a*d + b*c
         r%denominator = b*d
         intrat_add = simplify (r)
      end function


Sijoitus

Jos sijoituslauseen molemmat puolet samaa tyyppiä, ei tarvita lisämäärittelyja.

Jos sijoitus edellyttää epästandardia tyyppimuunnosta, sijoitusoperaattorin toiminta on määriteltävä.

Esimerkiksi sijoitus "rationaaliluku = kokonaisluku" ei ole automaattisesti määritelty, vaan moduulissa on kerrottava, miten tyyppimuunnos tehdään.

       interface assignment (=)
          module procedure intrat_subst
       end interface
       ...

       subroutine intrat_subst (p, i)
       ! sijoitetaan kokonaisluvun i arvo rationaaliluvuksi p
          integer, intent(in) :: i
          type (rational), intent(out) :: p
          p%nominator = i
          p%denominator = 1
          return
       end subroutine


Omat operaattorit

Voidaan määritellä mielin määrin omia operaattoreita.

Vain pisteellinen notaatio (kuten .eq., .and. jne)

       interface operator (.inv.)
          module procedure inverse
       end interface
       ...

       function inverse (p)
       ! lasketaan rationaaliluvun p kaanteisluku
       type (rational) inverse
       type (rational), intent(in):: p
       type (rational) r
       r%denominator=p%nominator 
       r%nominator=p%denominator
       inverse = simplify (r)
       return
     end function


Usein tarvittavat vakiot, tyyppimäärittelyt ja proseduurit kannattaa paketoida moduuleiksi.

Mieti, mitkä määrittelyt on syytä pitää yksityisinä. Käyttäjän ei tarvitse päästä suoraan käsiksi kaikkeen, koska seurauksena voi olla hankalasti jäljitettäviä virheitä. Erityisesti mutkikkaita tietorakenteita käsiteltäessä on parempi antaa käyttöön vain joukko testattuja ja luotettavia työkaluja.

Kannattaa harkita huolella, mitä omien muuttujatyyppien laskutoimituksia toteuttaa operaattoreina. Liiallinen operaattorien käyttö saattaa hämärtää käsitystä, mitä ohjelma oikeastaan tekee.

Laskutoimitusten tulisi olla luonnollisia (mitä puhelinluetteloiden kertolaskun pitäisi tehdä?)


Sitä sun tätä F90:stä

Osoittimet

Osoitin (pointer) on olio, joka viittaa johonkin muuttujaan:
           real, pointer, dimension (:) :: p
p on osoitin, joka voi osoittaa yksiulotteiseen taulukkoon. Taulukolle ei varata tilaa tässä vaiheessa.

Osoitin asetetaan osoittamaan muuttujaa operaattorilla =>.

Osoitin ei ole pelkkä muuttujan talletuspaikan osoite, vaan kuvaaja, joka sisältää muutakin tietoa. Osoitin voi viitata myös taulukkosektioon:

           real, dimension(100,100), target:: x
           real, pointer, dimension (:) :: p1
           real, pointer, dimension (:,:) :: p2

           p1 => x(1,:)
           p1 => x(:,10)
           p1 => x(1, 1:91:10)

           p2 => x
           p2 => x(2:3, 10:20)
           p2 => x(1:11:2, 10:100:10)
Fortranissa osoitin on lähinnä synonyymi sen osoittaman olion nimelle.

HUOM!

            p=p+1

C:ssä osoitin siirtyy osoittamaan seuraava alkiota.

Fortranissa p:n osoittamaan olioon lisätään 1.

Osoittimen tila (association status) voi olla:

        real, target :: x
        real, pointer :: p  
          ! p maarittelematon
        p => x              
          ! p on liitetty x:aan
        nullify(p)          
          ! p on tyhja
Jos p on liitetty johonkin muuttujaan, associated(p) on tosi.

Osoittimien käyttö dynaamiseen tilanvaraukseen:

        real, dimension(:), pointer :: p  
        ...
        allocate (p(1000))
        ...
        deallocate(p)
Sopii hyvin linkitettyihin tietorakenteisiin. Esimerkiksi binääripuu:
        type node
          character (len=100)  :: name
          type (node), pointer :: left, right
        end type node
        type (node), pointer :: root, l, r

        allocate(root, l, r)
        root%name='puun juuri'
        root%left => l      ! linkki vasempaan alipuuhun
        root%right => r     ! linkki oikeaan alipuuhun

Esimerkki: linkitetyn listan käsittely.

     program linkedlist
     ! rakennetaan ja tulostetaan lista, joka sisaltaa
     ! syotetyt luvut kasvavassa suuruusjarjestyksessa
     implicit none
     type node
       integer :: value
       type (node), pointer :: next
     end type node
     integer :: num
     type (node), pointer :: first, current, p, q
     ! first osoittaa listan alkuun
     ! luodaan aluksi tyhja lista
     nullify(first)
     ! luetaan alkioita ja lisataan listan alkuun
     ! lopetetaan, kun annettu luku on 0
     do
       read (*,*) num
       if (num==0) exit
       allocate(current)                 ! luodaan uusi tietue
       current%value=num                 ! asetetaan arvokentta
       ! liitetaan luotu tietue listaan oikealle paikalleen
       if (.not.associated(first)) then  
         ! lista on tyhja
         current%next=>first  
         first=>current
       else if (num <= first%value) then 
         ! luettu luku on pienempi kuin listan ensimmainen luku
         ! uusi tietue tulee listan alkuun
         current%next=>first  
         first=>current
       else 
         ! muuten ruvetaan etenemaan pitkin listaa
         ! uusi tietue p:n ja q:n osoittamien tietuiden valiin
         q=>first; p=>first%next
         do
           if (.not.associated(p)) exit  ! lista kayty loppuun
           if (num <= p%value) exit      ! loytyi oikea vali
           q=>p; p=>p%next               ! siirrytaan eteenpain
         end do
         ! linkitetaan tietue p:n ja q:n valiin
         current%next=>p  
         q%next=>current
       end if
     end do
     ! lista on valmis, tulostetaan 
     current=>first            ! listan 1. tietue
     ! edetaan listaa, kunnes vastaan tulee tyhja linkki
     do        
       if (.not.associated(current)) exit
       write (*,*) current%value
       current=>current%next  ! seuraava tietue
     end do
     end program linkedlist

Osoittimien mahdollisia ongelmia:

Roikkumaan jäänyt viite (dangling reference)

        real, pointer :: p, q
        ...
        allocate (p)
        p=1.0
        q=>p
        deallocate(p)    ! q = ?
Saavuttamaton muistialue:
        real, dimension(:), pointer :: p  
        ...
        allocate (p(1000))
        ...
        nullify(p)
Taulukolle on varattu tila, mutta siihen ei voi enää viitata millään keinolla. Tila vapautuu vasta ohjelman päättyessä.


Muuttujien attribuutit

Seuraava on täydellinen luettelo kaikista muuttujan attribuuteista.

dimension: määrittelee taulukon koon

parameter: muuttuja on vakio, jota ei saa muuttaa

save: muuttuja varataan staattisesti ja sen arvo säilyy aliohjelmakutsujen välillä

allocatable: dynaamisesti varattava taulukko

pointer: osoitin

target: muuttujaan saatetaan viitata osoittimen avulla

intent: aliohjelman argumentin käyttötapa

optional: valinnainen argumentti; voi puuttua proseduurin kutsusta

external: muuttuja on proseduurin nimi

intrinsic: muuttuja on varusfunktion nimi

private: moduulin yksityinen muuttuja

public: moduulin julkinen muuttuja


Syöttö ja tulostus

Namelist-rakenne Oppikirjan esimerkki s. 213
      program leivo
        implicit none
        integer :: jauhot=1, sokeri=1, munat=1
        namelist /kakku/ jauhot, sokeri, munat
        ...
        read(*, nml=kakku)
        ...
        write(*, nml=kakku)
      end program
Syöttötiedoston sisältö voisi olla
      &kakku munat=3, jauhot=4/
Tulostus:
      &KAKKU 
      JAUHOT=4, SOKERI=1, MUNAT=3
      /
Sopii esimerkiksi tilanteeseen, jossa useimmilla muuttujilla käyttökelpoiset alkuarvot. Syöttötietoina tarvitsee antaa vain muutettavat arvot.


Fourier'n muunnos

Funktion f Fourier'n muunnos F=Ff on

F(u)= \int-\infty\infty f(x) e-2\pi iux dx.

ja käänteismuunnos

f(x)=\int-\infty\infty F(u) e2\pi iux du.

Muitakin määritelmiä käytetään yleisesti. Integraalin edessä voi olla vakiokertoimia, kuten 1/2\pi tai 1/\sqrt(2\pi). Eksponentin tekijä 2\pi voi puuttua tai merkit voivat olla toisinpäin. Tarkista aina, mitä määritelmää milloinkin on käytetty!


Konvoluutio

Funktioiden h ja f konvoluutio on

(h * f)(x) = \int_-\infty\infty h(x-x') f(x') dx'.

Konvoluution Fourier'n muunnos on funktioiden muunnosten tulo:

F (h * f)(u) = (F h)(u) (F f)(u) = H(u)F(u).

Funktioiden f ja g korrelaatio Rfg on

Rfg = \int-\infty\infty f(x') g(x + x') dx'.

Funktion korrelaatio itsensä kanssa on autokorrelaatiofunktio:

Rff = \int-\infty\infty f(x') f(x+x') dx',

jonka Fourier'n muunnos on

F Rff = | F(u)|2.


Näytteenottolause (sampling theorem)

Jos funktion f Fourier'n muunnos häviää kaikille |u| > fc, f voidaan rekonstruoida täysin f:n arvoista pisteissä, joiden etäisyys on 1/2fc tai pienempi.

Jos signaali sisältää taajuuksia, jotka ovat suurempia kuin kaksi kertaa näytteidenottotaajuus, signaalia ei voi enää rekonstruoida yksikäsitteisellä tavalla. Nämä korkeat taajuudet saattavat aiheuttaa alias-ilmiön (aliasing), eli rekonstruoituun signaaliin ilmaantuu virheellisen tulkinnan vuoksi taajuuksia, joita alkuperäisessä signaalissa ei ole.


Diskreetti Fourier'n muunnos

Oletetaan, että muunnettava funktio tunnetaan tasavälein sijaitsevissa pisteissä fn=f(n\Delta x), n=0, ..., N-1. Se voidaan siten esittää N :n alkion vektorina f=(f0, ... , fN-1).

Diskreetti Fourier'n muunnos määritellään kaavalla

Fk = \sumn=0N-1 fn exp (-2\pi nik / N).

Käänteismuunnos on

fn= (1 / N) \sumk=0N-1 Fk exp (2\pi nik / N).

Normitustekijä 1/N esiintyy vain jommassakummassa muunnoksessa. Joskus sitä käytetään suoran muunnoksen lausekkeessa, jolloin se ei esiinny käänteismuunnoksessa.

Diskreetissä tapauksessa funktion ja sen muunnoksen arvoja esittävät luvut fn ja Fk liittyvät itse funktioiden f ja F arvoihin kaavoilla

fn = \Delta x f(n\Delta x),
Fk = F(k \Delta u),

missä

\Delta u \Delta x = 1 / N.


FFT (Fast Fourier Transform)

Menetelmä liitetään yleensä Cooleyn ja Tukeyn julkaisuun vuodelta 1965, vaikka vastaavanlaisia ajatuksia tunnettiin paljon aiemminkin (Gauss 1805). Parhaimmillaan menetelmä on, kun muunnettavan vektorin pituus on kakkosen potenssi, mutta algoritmista on olemassa versioita myös yleiselle tapaukselle.

Kun munnettavan vektorin alkioiden määrä N on kakkosen potenssi, muunnos voidaan laskea ajassa N log N.

Danielsonin ja Lanczosin lemma(1942):

Fk = \sumj=0N-1 fj exp (-2\pi ijk / N)
= \sumj=0N / 2-1 f2j exp (-2\pi i (2j)k / N) + \sumj=0N / 2-1 f2j+1 exp (-2\pi i (2j+1)k / N)
= \sumj=0N / 2-1 f2j exp (-2\pi i jk / (N /2) ) + Wk \sumj=0N /2-1 f2j+1 exp (-2\pi i jk / (N /2))
= Fkparillinen + Wk Fkpariton,

missä

W = e -2\pi i / N

Lasketaan siis erikseen parillisten ja parittomien alkioiden muunnokset, ja niistä yhdistetään lopullinen muunnos.

Kumpikin osa voidaan edelleen käsitellä samalla tavalla. Jatketaan rekursiivisesti, kunnes muunnettavana on vain yksi alkio.


program fft
  implicit none
  real, parameter :: pi=3.141592654
  integer, parameter :: m=2, n=2**m
  complex a(0:n)
  integer i

  a(0)=(0.0, 0.0) ;  a(1)=(1.0, 0.0) 
  a(2)=(2.0, 0.0) ;  a(3)=(3.0, 0.0) 

  write (*,*) 'alkuperainen data'
  do i=0,n-1
    write(*,'(2F8.4)') a(i)
  end do

  ! ensin suora muunnos
  call fourier(a, m, n, 1)
  write (*,*) 'suora muunnos'
  do i=0,n-1
    write(*,'(2F8.4)') a(i)
  end do

  ! sitten kaanteismuunnos
  call fourier(a, m, n, -1)
  write (*,*) 'kaanteismuunnos '
  do i=0,n-1
    write(*,'(2F8.4)') a(i)
  end do

  contains

subroutine fourier (a, m, n, sig)
!  kompleksimuuttujan Fourier'n muunnos
!  alkuperainen data on vektorissa a(0:n-1)
!  muunnettavan vektorin pituuden n on oltava 
!  kakkosen potenssi, n=2**m
!  muunnos korvaa alkuperaisen vektorin a(0..n-1)
!  jos sig < 0, lasketaan kaanteinen muunnos
  implicit none
  integer, intent(in) :: m, n, sig
  complex, dimension(0:n), intent(inout) :: a
  integer i,j,k,l, half_n, le,le1,ip
  real  angle 
  complex x,u,w,t 

  half_n = n/2

  ! jos kaanteismuunnos, konjugoidaan a
  if (sig < 0) a = conjg(a)

\page
  ! jarjestellaan vektori a s.e. a(s)=a(j), missa 
  ! s on j:n binaariesitys takaperin
  j=1 
  do i=1, n-1
    if (i < j)  then 
      x = a(j-1) ; a(j-1) = a(i-1) ;  a(i-1) = x ; 
    end if
    k = half_n 
    do while (k < j)  
       j = j-k ;  k = k/2 
    end do
    j = j+k
  end do

  ! muunnetaan palasia, joiden pituus on 
  ! le=2,4,8,...
  le=2
  do l=1, m
    le1 = le/2 
    u = cmplx(1,0)
    angle = pi/le1
    w = cmplx(cos(angle),sin(angle))
    do j=1,le1
      do i=j,n,le
        ip=i+le1 
        x = a(ip-1) 
        t = x * u
        a(ip-1) = a(i-1)-t
        a(i-1) = a(i-1)+t
      end do
      x = u * w
      u = x
    end do
    le = 2*le
  end do

  ! jos kaanteismuunnos, konjugoidaan a takaisin
  ! ja normitetaan 
  if (sig < 0) a=conjg(a)/n
end subroutine
end program

Ohjelman tulostus:
 alkuperainen data
  0.0000  0.0000
  1.0000  0.0000
  2.0000  0.0000
  3.0000  0.0000
 suora muunnos
  6.0000  0.0000
 -2.0000 -2.0000
 -2.0000  0.0000
 -2.0000  2.0000
 kaanteismuunnos 
  0.0000  0.0000
  1.0000  0.0000
  2.0000  0.0000
  3.0000  0.0000


Dekonvoluutio

Esimerkiksi kuva, jossa kuvauslaitteiston ja muiden tekijöiden aiheuttamia virheitä.

Oletetaan yksinkertaisuuden vuoksi, että kuvan leviäminen on samanlaista kaikkialla kuvan alueella. Havaittu kuva g on silloin konvoluutio alkuperäisestä kuvasta f ja kuvausjärjestelmän virhettä kuvaavasta funktiosta h, johon tulee lisäksi kohina n.

g (x, y ) = \int\int h (x-x', y-y') f(x',y') dx' dy' + n (x, y).

Integrointi ulotetaan tässä sellaisen yleensä pienen alueen yli, jonka ulkopuolella h ei enää poikkea merkittävästi nollasta.

Käytännössä kuva on diskreetti pistejoukko, jolloin integraali korvataan äärellisellä summalla

gij = \sumi' \sumj' hi-i', j-j' fi'j' + nij.

Joskus kuvaussysteemin aiheuttama häiriö eli konvoluution ydin h on tunnettu. Tätä funktiota kutsutaan myös nimellä point spread function eli PSF.

Läheskään aina PSF ei ole tiedossa. Mikäli mahdollista, havaintoihin kannattaisi sisällyttää mittaus, josta PSF voidaan selvittää. Sellainen voi olla esimerkiksi hyvin terävä pulssi, pistemäinen valonlähde tai sakara-aalto.

Dekonvoluutio suotimien avulla

Fourier'n muunnos muuttaa konvoluution kertolaskuksi:

G (u, v) = H (u, v) F (u, v) + N (u, v),

missä G, H, F ja N ovat funktioiden g, h, f ja n Fourier'n muunnokset. Tässä esiintyvää funktiota H kutsutaan usein systeemin siirtofunktioksi (transfer function).

Alkuperäinen kuva voidaan ainakin periaatteessa ratkaista:

F (u, v) = G (u, v) / H (u, v) - N (u, v) / H(u, v).

Tyypillisesti h on Gaussin funktio, ja sen Fourier'n muunnos on myös Gaussin funktio. Se poikkeaa oleellisesti nollasta vain hyvin pienellä alueella.

Hieman parempi ratkaisu on käyttää 1/H:n sijasta suodinta

M (u, v) = 1/H (u, v), jos u2 + v2 < r2,
M (u, v) = 1 muuten,

missä r on niin pieni, että H on varmasti nollasta poikkeava säteen r sisäpuolella.

Pienimmän neliösumman mielessä parhaan tuloksen antaa Wienerin suodin:

M (u, v) = [1 / H (u, v) ] [ |H (u, v)|2 / ( |H (u, v)2 + Snn (u, v) / Sff (u, v) ) ],

missä Snn ja Sff ovat kohinan ja alkuperäisen kuvan spektraalitiheydet. Käytännössä termiä Snn/Sff voi olla vaikea laskea, ja usein se korvataan vakiolla:

M (u, v) = [ 1 / H (u, v)] [ |H (u, v)|2 / ( |H (u, v)|2 + A ) ] = H*(u, v) / ( H*(u, v) H (u, v) + A ).

Vakio A on regularisointiparametri, joka tarkoituksena estää nimittäjää menemästä liian lähelle nollaa. Sopiva arvo löytyy usein helpoimmin kokeilemalla. Hyvin pienet arvot johtavat kyllä terävään kuvaan, mutta samalla vahvistavat kohinaa. Suuret arvot puolestaan tuottavat siistin mutta epätarkan kuvan.