태그 보관물: Fortran

Fortran config parser

포트란 언어에서 사용할 수 있는 configuration file parser 모듈을 github에 공개했습니다.

다음과 같은 configuration 파일에서 변수들을 읽어들일 수 있는 모듈입니다. 사용법은 github에 올렸습니다.

[DEFAULTS]
path = ../include
use_abs = True

[Section 1]
nmax = 30
# comment 1
vmin = 1.0
freqs = 5.0, 10.0, 30.0, 50.0
amps = 0.0, 1.0, 1.0, 0.0
path = ../text

[Section 2]
use_abs = no
; comment 2
my file = $(Section 1:path)/file.txt

SU 파일 입출력을 위한 포트란 라이브러리

포트란 사용자들이 Seismic Un*x의 SU 파일을 읽고, 수정하고, 쓸 수 있도록 하는 입출력 라이브러리를 작성하여 공개하였습니다.

Github에서 받으실 수 있습니다.

매뉴얼은 ReadTheDocs를 참고하세요.

참고문헌

하완수, 2015, SU 파일 입출력을 위한 포트란 라이브러리 개발, 한국자원공학회지, 52(1), 81-90.

Fortran option parser

gpl에서 사용하기 위해 개발한 포트란 option parser를 소개합니다. Option parser는 command line option들을 분석해서 프로그램에서 사용할 수 있도록 변수로 저장해주는 역할을 합니다. 리눅스나 맥에서 명령을 실행할 때

$ gcc -c -o main.e main.c
$ tar -zxvf file.tgz

와 같은 유닉스 표준 형태의 옵션이 있고,

$ suximage n1=101 d1=0.5 perc=99 < file.su

와 같이 Seismic Un*xMadagascar 등에서 사용하는 형태의 옵션도 있는데, 지금 소개해드리는 option parser는 두 번째 형태의 옵션을 지원합니다. Option parser는 한 번 작성한 프로그램의 재사용을 위해 매우 유용한 기능입니다. 우선 테스트 프로그램을 통해 사용 예를 살펴본 후 자세한 사용법을 보겠습니다.

테스트 프로그램

다음의 테스트 프로그램은 다양한 종류의 변수를 command line option으로부터 읽어서 출력하는 프로그램입니다.
지원하는 자료형은 integer, single precision real, double precision real, logical, character입니다(complex는 아직까지 필요가 없어서 안 넣었습니다). 단일 변수 뿐 아니라 각각의 배열도 지원합니다. 필수 입력 옵션과 기본값을 가진 옵션을 구분하여, 필수 입력 옵션들 중 하나라도 command line option에 없을 경우 도움말을 출력하고 프로그램을 종료합니다.

        program test_optparse
        use gpl_optparse
        implicit none
        integer,parameter:: mxlen=100,mxstr=100
        integer:: i,io,ia(mxlen)
        integer:: j,ni,nf,nb,nd,ns
        real:: f,fo,fa(mxlen)
        logical:: b,bo,ba(mxlen)
        real(kind=8):: d,d_o,da(mxlen)
        character(len=mxstr) :: s,so,sa(mxlen)

        ! required parameters
        call from_par('i',i,'integer number')
        call from_par('f',f,'float: single precision real number')
        call from_par('d',d,'double precision')
        call from_par('b',b,'boolean/logical')
        call from_par('s',s,'string')

        ! optional parameters
        call from_par('io',io,1,'1','optional integer')
        call from_par('fo',fo,1.0,'1.0','optional float')
        call from_par('do',d_o,1.0d0,'1.0d0','optional double')
        call from_par('bo',bo,.true.,'T','optional boolean')
        call from_par('so',so,'','empty','optional string')

        ! array, required parameters
        call from_par('ia',ia,ni,'integer array')
        call from_par('fa',fa,nf,'float array')
        call from_par('da',da,nd,'double array')
        call from_par('ba',ba,nb,'boolean array')
        call from_par('sa',sa,ns,'string array')

        call help_par()
        !call report_par()
        
        print*,'i=',i
        print*,'f=',f
        print*,'d=',d
        print*,'b=',b
        print*,'s=',trim(s)
        print*,''
        print*,'ia=',ia(1:ni)
        print*,'fa=',fa(1:nf)
        print*,'ba=',ba(1:nb)
        print*,'da=',da(1:nd)
        print*,'sa=',(trim(sa(j))//'_',j=1,ns)
        print*,''
        print*,'io=',io
        print*,'fo=',fo
        print*,'do=',d_o
        print*,'bo=',bo
        print*,'so=',so
        end program

실행 결과

위의 프로그램을 컴파일하여 아래와 같이 실행하면 변수들이 제대로 입력된 것을 확인할 수 있습니다. 문자열의 경우 문자열을 둘러싼 따옴표는 제거하고 변수에 저장합니다. 배열은 쉼표를 기준으로 배열의 원소를 나눕니다. 기본값을 가지고 있는 변수의 경우 command line option에 있으면 주어진 값을 저장하고 없으면 기본값을 저장합니다.

$ ./test_optparse s=test.dat i=2 f=2.5 d=3.0 b=T ia=1,2,3,4 fa=1.0,2.0,3.0,4.0 da=1.,2.,3. ba=T,F,T sa=st,'ri',"ng" io=5 so="gpl"
 i=           2
 f=   2.50000000    
 d=   3.0000000000000000     
 b= T
 s=test.dat
 
 ia=           1           2           3           4
 fa=   1.00000000       2.00000000       3.00000000       4.00000000    
 ba= T F T
 da=   1.0000000000000000        2.0000000000000000        3.0000000000000000     
 sa=st_ri_ng_
 
 io=           5
 fo=   1.00000000    
 do=   1.0000000000000000     
 bo= T
 so=gpl

만약 기본값이 없는 필수 옵션 중 하나라도 command line option에 빠져있다면 다음과 같이 도움말을 표시하고 프로그램을 종료합니다.

$ ./test_optparse 
 Required parameters:
     [i] i=             : integer number
     [f] f=             : float: single precision real number
     [d] d=             : double precision
     [b] b=             : boolean/logical
     [s] s=             : string
     [I] ia=            : integer array
     [F] fa=            : float array
     [D] da=            : double array
     [B] ba=            : boolean array
     [S] sa=            : string array
 Optional parameters:
     [i] io=1           : optional integer
     [f] fo=1.0         : optional float
     [d] do=1.0d0       : optional double
     [b] bo=T           : optional boolean
     [s] so=empty       : optional string

위의 도움말에서 i=integer, f=real, d=real(kind=8), b=logical, s=character(len=?)을 의미하고, 각각의 대문자는 배열을 의미합니다.

사용법

그럼 실제 모듈의 사용법을 알아보겠습니다. 모듈은 use gpl_optparse 또는 간편하게 use gpl로 불러올 수 있습니다. 가장 중요한 from_par 서브루틴은 다음과 같이 세 가지 방식으로 사용할 수 있습니다.

call from_par('parname',variable,'help message') !! 필수 옵션
call from_par('parname',variable_arr,len_arr,'help message') !! 필수 배열
call from_par('parname',variable,default,'default message','help message') !! 기본값이 있는 변수

Command line option은 parname=value 형태로 입력 받게 됩니다. 위의 서브루틴들에서 'parname'은 이 때 사용되는 이름이고, valuevariable에 저장됩니다. ’help message’는 도움말 출력시 보여주는 변수 설명입니다.

배열의 경우 parname=value1,value2,value3과 같은 형태로 입력받고, 입력받은 값은 variable_arr에 저장됩니다. 이 때 입력받은 원소는 len_arr 개입니다.

기본값이 있는 변수의 경우 default는 기본값이고, 'default message'는 도움말 출력시 기본값을 보여주기 위한 문자열입니다.

함수 오버로딩을 사용하였기 때문에 정수, 실수 등의 자료형에 상관없이 위의 서브루틴들을 이용할 수 있습니다.

기타 사용할 수 있는 서브루틴들과 함수는 다음과 같습니다.

call help_par()
call help_header('msg before the parameter help msg')
call help_footer('msg after the parameter help msg')
call force_help()

call report_par()

if(given_par('parname')) call do_something()
call from_parfile('parfile.txt')

에서 help_par는 도움말 출력을 위한 서브루틴입니다. 도움말과 관련된 서브루틴들로는 도움말을 보강하기 위한 call help_header('msg'), call help_footer('msg'), 필수 옵션이 주어졌는지와 무관하게 도움말을 출력하기 위한 call force_help()가 있습니다. report_par는 입력받은 변수들을 출력해서 확인하기 위한 서브루틴입니다. 이 외에도 옵션이 주어졌는지 확인하기 위한 logical function given_par('parname') 함수, 옵션들을 저장해놓은 텍스트파일로부터 옵션들을 읽어들이기 위한 call from_parfile('parfile.txt')와 같은 명령이 있습니다.

참고로, command line option에 par=parfile.txt와 같은 옵션이 있으면 parfile.txt에서 먼저 변수를 읽은 후 command line option을 읽습니다. parfile.txt에 주어진 변수가 command line option에 다시 나오면 command line에 주어진 값을 사용합니다.

gpl_optparse 모듈의 소스코드는 gplGitHub에서 받으실 수 있습니다.

Polymorphic Fortran & C

포트란 함수 오버로딩과 중복

앞서 포트란 함수 오버로딩에 관한 글을 올렸습니다. 포트란 모듈과 인터페이스를 이용하면 서로 다른 자료형을 인자로 받는 함수나 서브루틴이라도 같은 이름으로 사용할 수가 있었습니다. 그런데, 동적 자료형을 지원하는 언어와 달리, 포트란에서는 자료형별로 서브루틴들을 따로 만든 후에 같은 이름으로 호출하였습니다. 동적 자료형 언어에서는 함수 자체를 한 번만 작성하면 되지요. 여기에서 포트란 코드 작성에 중복이 발생하게 됩니다. 이러한 중복을 제거하기 위해 파이썬으로 만든 스크립트가 polyfc (Polymorphic Fortran & C)입니다. 예전에 Forpedo에서 아이디어를 얻었는데, Forpedo를 사용하려니 좀 복잡해서 Python 연습도 할 겸 gpl용으로 만들었습니다.

예제

이해하기 쉽게 예제를 살펴보겠습니다. 배열의 내용을 출력하는 서브루틴을 작성하려고 합니다. 배열은 integer, real, real(kind=8), complex, complex(kind=8) 다섯 종류의 자료형을 지원하려고 합니다. 그럼 서브루틴을 자료형에 따라 총 5개를 작성해야 하는데, 선언부만 다르고 나머지는 동일하거나 거의 비슷하게 됩니다. 그래서 일종의 템플릿 서브루틴을 만들고, 필요한 부분만 바꿔가며 서브루틴을 복제하려고 합니다. 그러면 거의 비슷한 코드를 중복해서 작성하는 수고를 덜 수 있겠죠. 아래는 polyfc에 입력으로 들어가는 템플릿 파일입니다.

module polyfc_example

!@interface print_array
contains
!@template print_array ifdcz
    subroutine print_array_<name>(arr)
    <type>,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
!@end
end module

포트란 주석을 이용하여 인터페이스가 들어갈 부분과 템플릿 부분을 표시하고, 자료형에 따라 바뀌어야 하는 부분은 <name>, <type>으로 표시하였습니다. 이 외에 필요할 경우 <kind><esize>도 지원합니다. 바뀌며 들어가는 부분은 다음 표를 보면 알 수 있습니다.

Name Fortran type C type Fotran kind esize
i integer int (kind=4) 4
f real float (kind=4) 4
d real(kind=8) double (kind=8) 8
c complex float complex (kind=4) 8
z complex(kind=8) double complex (kind=8) 16
b logical (not supported) (kind=4) 4
s character(len=*) char (len=*) 1

그래서 polyfc input.f90 > output.f90과 같이 실행했을 때 얻게 되는 파일은 다음과 같습니다.

! This file was generated from pfc.example.f90 by Polyfc at Mon Jul 28 22:12:32 2014.
! Do not edit this file directly.

module polyfc_example

    interface print_array
        module procedure print_array_i
        module procedure print_array_f
        module procedure print_array_d
        module procedure print_array_c
        module procedure print_array_z
    end interface print_array

contains

    subroutine print_array_i(arr)
    integer,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_f(arr)
    real,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_d(arr)
    real(kind=8),intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_c(arr)
    complex,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_z(arr)
    complex(kind=8),intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
end module

자동으로 작성하면 손으로 복사했을 경우 생길 수 있는 오류도 피할 수 있겠죠. 단, 코드에 버그가 있을 때에는 output 파일에서 버그가 있는 곳을 찾아 input 파일의 해당 위치를 고쳐줘야 합니다.

참고로, polyfc 이름이 의미하듯, C도 지원합니다. 단, C에서는 <name><type>만 지원합니다. 물론 인터페이스도 지원하지 않습니다. C에서는 아래와 같은 방식으로 사용 가능합니다.

//@template ifdczbs
void abc_<name>(<type> def)
{
    ...
}
//@end

Unit number 자동 할당

포트란에서 파일을 열 때 파일에 번호(Logical unit number)를 할당하고 그 번호를 이용하여 파일 내용을 읽거나 파일에 출력을 하게 됩니다. 리눅스에서 다음 세 번호는 기본적으로 할당이 되어(입출력 스트림이 열려) 있습니다.

0: stderr
5: stdin
6: stdout

나머지 4바이트 정수 자료형으로 표현할 수 있는 양의 정수값은 사용자가 파일에 할당하여 사용할 수 있습니다 (컴파일러마다 범위는 차이가 있습니다). 보통은 사용하는데 문제가 없지만 하나의 프로그램에서 여러 개의 파일을 열 경우 앞에서 다른 파일에 할당하여 사용중인 번호를 피해 새로운 번호를 찾아야 합니다. 사용중인 번호를 피하기 위해 앞의 코드를 살펴본다든지, 파일 입출력 서브루틴을 위해 새로운 번호를 전달해줄 경우 귀찮다는 생각이 들게 됩니다. 예를 들면 다음과 같은 경우죠.

subroutine write_a_number(un, filename, n)
integer,intent(in):: un, n
character(len=*),intent(in):: filename
open(un,file=trim(filename))
write(un,*) n
close(un)
end subroutine

이럴 때 inquire함수를 사용하면 특정 번호가 사용중인지 알 수 있고, 따라서 새로운 번호도 자동으로 찾을 수 있습니다. 아래 서브루틴과 함수는 gpl의 포트란 모듈(module_base)에서 복사하였습니다. 파일 번호는 99번부터 10번까지만 사용하게 만들었습니다. 동시에 그 이상의 파일을 열 일은 없다고 생각하고 만들었는데, 많은 파일을 열어야 할 경우 do loop 범위를 조정하면 되겠습니다.

subroutine assign_un(un)
integer, intent(out) :: un
logical :: oflag
integer :: i
do i=99,10,-1
    inquire(unit=i,opened=oflag)
    if(.not.oflag) then
        un=i
        return
    endif
enddo
stop "Error: Logical unit assignment"
end subroutine

logical function un_opened(un) result(val)
integer,intent(in):: un
inquire(unit=un,opened=val)
end function

위의 서브루틴을 사용하면 앞에서 살펴본 write_a_number 서브루틴을 다음과 같이 고칠 수 있습니다. 서브루틴의 인터페이스가 좀 더 간단해졌고, 앞으로는 서브루틴을 사용할 때 파일 번호에 대해 고민할 필요가 없겠죠. 자동화의 간단한 예가 되겠습니다.

subroutine write_a_number_new(filename, n)
integer,intent(in):: n
character(len=*),intent(in):: filename
integer:: un
call assign_un(un)
open(un,file=trim(filename))
write(un,*) n
close(un)
end subroutine

포트란과 연산자 오버로딩

함수 오버로딩과 마찬가지로, 연산자 오버로딩도 객체지향 프로그래밍의 다형성과 관련된 개념입니다. 포트란 90에서 연산자 오버로딩을 사용하는 방법을 살펴보겠습니다. 아래 코드는 이차원 좌표 자료형과 두 개의 점을 더하는 함수 예제입니다.

module point2d_op

type point
    real x, y
end type

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3=add(p1,p2)

print*, p3%x,p3%y
end program

새로운 자료형을 정의했으니 새로운 자료형에 대응하는 더하기 연산도 따로 정의할 필요가 있습니다. 그런데 p3=add(p1,p2)와 같이 쓰는 것보다는 p3=p1+p2로 쓰는 것이 더 직관적이고 이해하기 쉽겠죠. 이 때 사용하는 것이 연산자 오버로딩입니다.

연산자 오버로딩

포트란에서는 interface문을 사용하여 이미 존재하는 연산자를 오버로드 하거나 새로운 연산자를 정의할 수 있습니다. 아래 예제는 + 연산자를 오버로드 하는 예제입니다.

module point2d_op

type point
    real x, y
end type

interface operator (+) 
    module procedure:: add 
end interface

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3 = p1 + p2

print*, p3%x,p3%y
end program

수학 연산자를 오버로딩할 때에는 수학적 정의에 합당하도록 또는 이해하기 쉽도록 정의하는 것이 좋습니다. 위의 add 함수를 - 연산자에 오버로딩하는 것도 가능하나 그렇게 되면 프로그래밍할 때 문제가 발생하겠죠.

새로운 연산자 정의

연산자를 오버로드하지 않고 새로 정의할 수도 있습니다. 새로 정의하는 연산자는 .name.과 같이 '.'으로 시작해서 '.'으로 끝나야 합니다. 아래 예제는 add 함수를 .add. 연산자로 정의한 경우입니다. p3=add(p1,p2) 대신 p3=p1.add.p2와 같이 사용한 것을 볼 수 있습니다.

module point2d_op

type point
    real x, y
end type

interface operator (.add.) 
    module procedure:: add 
end interface

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3 = p1 .add. p2

print*, p3%x,p3%y
end program

포트란과 함수 오버로딩

포트란과 객체지향 프로그래밍

포트란이라 하면 옛날 언어라고 생각하기 쉬운데, 포트란 언어도 시대의 흐름에 따라 계속 발전해 오고 있습니다. 특히, 2003버전부터는 클래스와 상속 등을 지원하는 객체지향 프로그래밍 언어라 할 수 있습니다. 안타깝게도, 한글로 된 포트란 서적이 별로 없고, 출판된 것도 95버전까지밖에 안 나와 있어서 앞으로 블로그에서 몇 가지 객체지향 프로그래밍과 관련된 기능들을 소개하고자 합니다.

포트란과 함수 오버로딩

함수 오버로딩이란 같은 이름의 서브프로그램(서브루틴, 함수, 메소드 등)이 인자에 따라 다른 기능을 하는 것을 말합니다. 객체지향 프로그래밍의 다형성과 관련된 개념인데, 포트란에서는 90버전에 도입된 interface문을 이용하여 적용 가능합니다.

아래 예제는 2차원 좌표라는 자료형을 정의하고 좌표들 사이의 거리를 구하는 함수를 구현한 예제입니다. 자료형을 정의할 때 single precision 자료형 point_sp와 double precision 자료형 point_dp 두 가지를 정의했고, 따라서 함수도 single precision용 distance_sp와 double precision용 distance_dp 두 가지를 정의했습니다.

module point2d

type point_sp
    real x, y
end type

type point_dp
    real(kind=8):: x,y 
end type

contains

    real function distance_sp(p1, p2) result(dist)
    type(point_sp), intent(in):: p1, p2
    dist=sqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function

    real(kind=8) function distance_dp(p1, p2) result(dist)
    type(point_dp), intent(in):: p1, p2
    dist=dsqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function 

end module

program test_point2d
use point2d
type(point_sp):: s1,s2
type(point_dp):: d1,d2
real:: dist_s
real(kind=8):: dist_d

s1%x=0.0 ; s1%y=0.0
s2%x=3.0 ; s2%y=4.0

d1%x=0.0d0 ; d1%y=0.0d0
d2%x=3.0d0 ; d2%y=4.0d0

dist_s=distance_sp(s1,s2)
dist_d=distance_dp(d1,d2)

print*, dist_s, dist_d
end program

위의 예제에서 두 개의 함수가 하는 일은 같지만, 정적 자료형 언어의 특성상 자료형에 따라 두 개의 함수를 정의해야 했습니다. 이 프로그램은 간단해서 별 문제가 없지만, 프로그램이 복잡해지면 이렇게 같은 기능의 다른 함수들로 인해 인터페이스가 복잡해지게 됩니다. 같은 기능의 함수들을 single/double과 같은 자료형에 상관없이 같은 이름으로 사용할 수 있다면 프로그래밍 인터페이스가 좀 더 단순해지겠죠. 위의 모듈에 아래의 interface 구문을 넣으면 distance_sp 함수와 distance_dp 함수를 모두 distance라는 이름으로 사용할 수 있습니다.

interface distance
    module procedure:: distance_sp
    module procedure:: distance_dp
end interface

위와 같이 선언해 놓으면 두 함수의 인자가 두 개의 point_sp와 두 개의 point_dp로 다르기 때문에 인자의 자료형을 가지고 어떤 함수를 사용해야하는지 판단할 수 있게 됩니다. 만약 두 함수의 인자가 동일하다면 interface문을 사용할 수 없습니다. 아래는 함수 오버로딩을 사용하도록 수정한 예제입니다.

module point2d

type point_sp
    real x, y
end type

type point_dp
    real(kind=8):: x,y 
end type

interface distance
    module procedure:: distance_sp
    module procedure:: distance_dp
end interface

contains

    real function distance_sp(p1, p2) result(dist)
    type(point_sp), intent(in):: p1, p2
    dist=sqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function

    real(kind=8) function distance_dp(p1, p2) result(dist)
    type(point_dp), intent(in):: p1, p2
    dist=dsqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function 

end module

program test_point2d
use point2d
type(point_sp):: s1,s2
type(point_dp):: d1,d2
real:: dist_s
real(kind=8):: dist_d

s1%x=0.0 ; s1%y=0.0
s2%x=3.0 ; s2%y=4.0

d1%x=0.0d0 ; d1%y=0.0d0
d2%x=3.0d0 ; d2%y=4.0d0

dist_s=distance(s1,s2)
dist_d=distance(d1,d2)

print*, dist_s, dist_d
end program

자료형에 상관없이 distance라는 함수를 이용하여 거리를 계산하고 있음을 알 수 있습니다. 물론 위에서 distance_spdistance_dp도 사용 가능합니다. 함수 오버로딩을 사용한다고 프로그램의 실행 속도가 빨라지지는 않습니다. 하지만 서브프로그램들의 인터페이스를 단순화하여 프로그래밍을 좀 더 편하게 할 수 있다는 장점이 있습니다.