LoginSignup
0
2

fortranにおいてpythonをメインプログラムにするには?

Last updated at Posted at 2024-01-24

前提: fortranの仕事はmodule内にデータを貯めること

fortranは数値計算に適しているが、pythonのもつ自由度がない。将来的にはpythonを親に据えたコードに書き換えていく必要がある。上位でのMPI制御はpython側で行うべき。

fortranで巨大プログラムを書くのはやめたほうがいいーmoduleによるsingletonしかできないものだと悟るべき。すなわち、fortranの仕事を「fortranのmodule内にデータを貯める」という操作だけにとどめるべきである.fortranのmodule内subroutineは指示のみを引数として受け取り、その指示に従ってデータをmodule内に貯めこむという操作のみを行う(protectedで書く)。データは他のmoduleやファイルから読み込む。これを繰り返して計算を行うことになる。(将来的にはfortran code内でMPI関数のコールはしないほうがいいと思われる。)

fortranをsingletonパターンを要請する「上記のことしかできない数値計算専用プログラミング言語」として見るのならそんなに悪くはない。むしろ特化している分、ムダなお祈り文を書かなくてすむ。正直、fortranはその路線で進化すべきであり、汎用言語として進化する必要はないと思う。

pythonからfortranを呼び出すサンプル

こういうことになると、およそ単純なsubroutine callの繰り返しで計算が進行していくことになる。ただしpythonからライブラリ化したfortranのsubroutineを呼ばないといけない。またMPIはpython側で起動しcommunicatorをfortranに渡さないといけない。

そのサンプルを書いてみた.メインプログラムhello.pyは非常に簡単になる。

# main program hello.py
from setcomm import *
ecaljF = setcomm("ecaljF.so")
callF( ecaljF.hello )
callF( ecaljF.hello )
callF( ecaljF.hello3, [True, 34, 8.2, 9.3] )

このサンプルでは,fortranのsubroutine helloとsubroutine hello3を呼び出している。以下がそのソースコードである。これらのsubroutineはインプット引数しか許さないし、pythonへの返り値はなし。fortranコードの書換として必要なのは「fortran側ではbind(C)をつけること。MPIのsize,rank,communicatorはmodule m_commの変数を読む」という点のみであるので単純である。関数呼び出しのとき、module内関数はグローバルにアクセスできるものとなっている点には注意がいる(useみたいなものを指定しない)。

#fortran subroutines 
module m_test1
  use m_comm,only: size,rank
  !include "mpif.h"
  integer,protected:: iii,n
  logical:: init=.true.
  real(8):: a=3.21
  complex(8),allocatable:: ccc(:,:)
  private
  !public hello
contains
  subroutine hello() bind(C)
    implicit none
    complex(8):: cc
    if(init) then
       print *,'rank/size=',rank,size, "Hello World111init"
       init=.false.
       a=1.23d0
       n=1000
       cc=rank
       allocate(ccc(n,n),source=cc)
    else
      print *,'rank/size=',rank,size, "Hello World111",a,sum(ccc)/n/n
    endif
  end subroutine hello
end module m_test1
module m_test2
contains
 subroutine hello3(lll,intx,r1,r2) bind(C)
   use m_comm,only: comm,size,rank
   implicit none
   include "mpif.h"
   integer :: ierr,intx
   logical:: lll
   real(8):: r1,r2
   call MPI_barrier(comm,ierr)
   if(mod(rank,2)==0) print *,'rank/size=',rank,size, "Hello World333 even", lll,intx,'sum=',r1+r2
   if(mod(rank,2)==1) print *,'rank/size=',rank,size, "Hello World333 odd ", lll,intx,'sum=',r1+r2
 end subroutine hello3
end module m_test2

hello()ではテストのためallocationなどmodule変数へのデータ格納を試している。

上の、python-fortranの結合のからくりにおいては関数setcomm("ecaljF.so")がミソになる。これは以下の2つのファイルsetcomm.pyとm_comm.pyで定義されている。

# setcomm.py
from ctypes import *
import numpy as np
from mpi4py import MPI

def setcomm(aaa): #Pass communicator to module m_comm.f90
    comm = MPI.COMM_WORLD
    comm = comm.py2f()
    eee = np.ctypeslib.load_library(aaa,".")
    eee.setcomm.argtypes = [ POINTER(c_int32) ]
    #eee.setcomm.restype  = c_void_p
    eee.setcomm(c_int32(comm))
    return eee

def callF(eee,mmm=[]):
    argtypess=[]
    data=[]
    for ii in  mmm:
        #print(ii,type(ii))
        if(type(ii)==type(1)): 
            argtypess.append( POINTER(c_int32) )
            data.append(c_int32(ii))
        elif(type(ii)==type(1.0)):
            argtypess.append( POINTER(c_double) )
            data.append(c_double(ii))
        elif(type(ii)==type(True)):
            argtypess.append( POINTER(c_bool) )
            data.append(c_bool(ii))
    eee.argtypes= argtypess
    if(len(argtypess)==0):
        eee()
    else:
        eee(*data)
    return
# m_comm.f90
module m_comm
  integer,protected:: comm,size,rank
contains
  subroutine setcomm(commin) bind(C)
    implicit none
    include "mpif.h"
    integer :: ierr,n,commin
    call MPI_Comm_size(commin, size, ierr)
    call MPI_Comm_rank(commin, rank, ierr)
    comm=commin
  end subroutine setcomm
end module m_comm

汎用コードであるので、fortranユーザーはいちいちsetcomm.pyとm_comm.pyをいじる必要はない。function呼び出しなどにも拡張できる。正直、十分に単純なのでコンパイラー&リンカーがトラブルを起こすことは少ないと思う。

一回目のコールのcallF( ecaljF.hello )でモジュール変数を貯めており
2回目のcallF( ecaljF.hello )のコールでそれを参照して違う結果を与えることを考えることができる。
hello.pyはfortranでも簡単に書き換えれて

#hello.f90
program hello
use m_comm,only: setcomm
implicit none
include "mpif.h"
integer:: info
call MPI_Init( info )
call setcomm(MPI_COMM_WORLD)
call hello()
call hello()
call hello3(True, 34, 8.2d0, 9.3d0)
end

となる。fortranで完結させて動作チェックした上でpython化しないといけないので、この単純さとhello.pyとの間の単純な対応関係は重要である。
このレベルであれば、hello.f90をhello.pyへ自動書き換えを行うスクリプトも単純に書ける思われる。

ソース

https://github.com/tkotani/PythonFortran/blob/master/hello.py
に置いた。READMEがあるが、

 mpif90 -shared -fPIC -o ecaljF.so *.f90 #ecaljF.soを作る
 mpiexec -n 4 python3 ./hello.py         #4coreでMPI実行      

で実行できる。

そもそもpythonがfortranに指示を送ってfortranのモジュール内にデータを貯め込む、という設計思想を守るべきなので、python-fortranの間で複雑なデータのやり取りはさせない。pythonにプロットさせたければ、fortranでファイルを書かせてそれを受け取って書かせるので良い。そんなところでのバイナリレベルでの結合に時間を費やすのは時間のムダだしトラブルのもと。とくにfortran側を書き換えたくない。

ただそうは言っても教条主義的なのが一番いかんことだし、もう少し改良してfortranのreal(8),integerの一次元配列ぐらいは同じやり方で渡せるようにしたい。さらには

# main program hello.py
from setcomm import *
ecaljF = setcomm("ecaljF.so")
callF( ecaljF.hello )
callF( ecaljF.hello )
callF( ecaljF.hello3, [True, 34, 8.2, 9.3, array1input],[out1,out2] )

などと書けるとうれしいな。どうやればいいのかな?(1.fortranのほうはcomm参照とbind(C)以外書き換えたくない。2.pythonと結合しないと動かないようなfortranコードでは困るし、そのために大きく書き換えないといけないのでは困る。3.返り値out1,out2も受け取れるならその方が良い。)

いいのができたら教えてほしいです

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2