🌈

Juliaから外部プログラムを実行する

2021/09/01に公開

https://docs.julialang.org/en/v1/manual/calling-c-and-fortran-code/
によると, JuliaではCやFortranのルーチンをccallで呼び出せます. しかし, 共有ライブラリとしてコンパイルしないとけないので, 標準入力と標準出力でデータをやり取りするメインプログラムを呼び出すことは難しいように思われます. 元のプログラムには手を加えないで, Julaiからプログラムを呼び出せるようにすることがこのノートのテーマです. CやFortranに限らず, 外部プログラムを呼び出す方法は
https://docs.julialang.org/en/v1/manual/running-external-programs/
に概ね書いてありますが, サンプルが少なく, Windows環境だといろいろ引っかかるポイントがあるので補足説明していきます. 環境は以下の通りです.

versioninfo()
Julia Version 1.6.2
Commit 1b93d53fc4 (2021-07-14 15:36 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: Intel(R) Core(TM) i7-4650U CPU @ 1.70GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-11.0.1 (ORCJIT, haswell)

Hello World!

さっそくですがHello World!していきましょう. 戻り値が不要な(標準出力で良い)場合はrun(), 結果を受け取りたい場合はread()readchomp()です. なお, コマンドを囲むのはバッククォートです.

run(`echo hello`)
read(`echo hello`, String)
readchomp(`echo hello`)
run(`cmd /C echo hello`)
hello
read(`cmd /C echo hello`, String)
"hello\r\n"
readchomp(`cmd /C echo hello`)
"hello"
run(`powershell /C echo hello`)
hello
read(`powershell /C echo hello`, String)
"hello\r\n"
readchomp(`powershell /C echo hello`)
"hello"

型はそれぞれ以下のようになります. 出力にhelloが挟まりますが気にしないでください.

println("1. ", typeof(`cmd /C echo hello`))
println("2. ", typeof(run(`cmd /C echo hello`)))
println("3. ", typeof(read(`cmd /C echo hello`, String)))
println("4. ", typeof(readchomp(`cmd /C echo hello`)))
1. Cmd
hello
2. Base.Process
3. String
4. SubString{String}

メモ帳notepad.exeなどのプログラムを呼び出すこともできます.

run(`notepad`)
run(`notepad.exe`)
run(`notepad.exe program1.f90`)

標準出力のみ

Hello World!と同じですが, 一応確認しましょう. 次のprogram1.f90は4という数値を標準出力に返すだけのプログラムです. コンパイルして生成されたprogram1.exeを呼び出して標準出力の動作を確認していきます. (ファイルはリポジトリにあります. gfortranがインストールされてパスが通っていればcompile.batをクリックすると勝手にコンパイルされます.)

program1.f90
program main
  write(6,*) 4
end program main
run(`program1`);
           4
run(`program1.exe`);
           4
run(`cmd /C program1.exe`)
           4
read(`program1.exe`, String)
"           4\r\n"
readchomp(`program1.exe`)
"           4"

標準入力から変数を1つ渡す

次のprogram2.f90は標準入力をread文で読み取り, 数値を2乗して標準出力に返すプログラムです. コンパイルして生成されたprogram2.exeを呼び出して動作を確認していきます. (ファイルはリポジトリにあります. gfortranがインストールされてパスが通っていればcompile.batをクリックすると勝手にコンパイルされます.)

program2.f90
program main
  implicit none
  integer x
  read(5,*) x
  write(6,*) x**2
end program main
input2.txt
5

まず, コマンドラインから<によって標準入力を渡せますが, <'<'のようにシングルクォーテーションで囲います.

run(`cmd /C program2.exe '<' input2.txt`)
          25

コマンドプロンプトを経由せずに実行するにはpipeline()を使います. 引数stdinにファイル名やコマンドを与えることができます. ファイル名の時はダブルクォーテーション, コマンドはバッククォートなので気を付けてください.

run(pipeline(`program2.exe`, stdin="input2.txt"))
          25
run(pipeline(`program2.exe`, stdin=`cmd /C echo 6`))
          36

以下のようにopen文で標準入力を渡すこともできます. 恐らく, 結果を受け取れないようなのでrunでよいと思います. 強いて言うなら, openendの間でfor文を回したり, write, print,printlnなどが使い分けられるなどのメリットがあります.

open(`program2.exe`, "w", stdout) do io
   println(io, 7)
end
          49
io = open(`program2.exe`, "w", stdout)
println(io, 7)
close(io)
          49

標準入力から変数を2つ渡す

次のprogram3.f90は標準入力をread文で読み取り, 2つの数値の和を取って標準出力に返すプログラムです. 基本的には先ほどの例と同じですが, Fortran側のプログラムが2つの数値を読み取れるようになっています. コンパイルして生成されたprogram3.exeを呼び出して動作を確認していきます. (ファイルはリポジトリにあります. gfortranがインストールされてパスが通っていればcompile.batをクリックすると勝手にコンパイルされます.)

program3.f90
program main
  implicit none
  double precision x, y
  read(5,*) x, y
  write(6,*) x + y
end program main
input3.txt
5
6
run(pipeline(`program3.exe`, stdin="input3.txt"))
   11.000000000000000     
run(pipeline(`program3.exe`, stdin=`cmd /C echo 5 7`))
   12.000000000000000     
open(`program3.exe`, "w", stdout) do io
   println(io, "5 8")
end
   13.000000000000000     
input = "5
9"

open(`program3.exe`, "w", stdout) do io
   println(io, input)
end
   14.000000000000000     
open(`program3.exe`, "w", stdout) do io
   println(io, 5)
   println(io, 10)
end
   15.000000000000000     

配列として渡したい場合には以下のように対処します.

input = [5,11]
run(pipeline(`program3.exe`, stdin=`cmd /C echo $input`))
   16.000000000000000     
input = [5,12]
open(`program3.exe`, "w", stdout) do io
    for x in input
        println(io, x)
    end
end
   17.000000000000000     

外部プログラムを関数として扱う

先ほどのprogram3.exeを例に解説します. このプログラムを関数として扱うには, 戻り値が必要なのでread()readchomp()を利用します. これらの結果は文字列なので, さらにparse()を使って数値に型変換します.

f(X) = parse(Float64,readchomp(pipeline(`program3.exe`, stdin=`cmd /C echo $X`)))
y = f([5,13])
18.0

外部プログラムのパラメータ最適化

ここではOptim.jlを利用します. 事前にパッケージモードadd Optimを実行してインストールし, ノート上ではusing Optimを宣言しておく必要があります. まず, Rosenbrock関数を最小化する例を見てみましょう.

# using Pkg
# Pkg.add("Optim")
using Optim
rosenbrock_julia(x) = (1.0 - x[1])^2 + 100.0 * (x[2] - x[1]^2)^2
x0 = [0.0, 0.0]
opt = optimize(rosenbrock_julia, x0)

println(Optim.minimizer(opt))
println(Optim.minimum(opt))
[0.9999634355313174, 0.9999315506115275]
3.5255270584829996e-9

次のprogram4.f90は標準入力から変数x,yを受け取ってRosenbrock関数の値を標準出力に返す例です. このプログラムをコンパイルしたprogram4.exeをJuliaの関数としての扱い, x,yを最適化します.

program4.f90
program main
  implicit none
  double precision x, y
  read(5,*) x, y
  write(6,*) 100.*(x*x-y)**2+(1.-x)**2
end program main
rosenbrock_fortran(x) = parse(Float64,readchomp(pipeline(`program4.exe`, stdin=`cmd /C echo $x`)))
x0 = [0.0, 0.0]
opt = optimize(rosenbrock_fortran, x0)

println(Optim.minimizer(opt))
println(Optim.minimum(opt))
[0.9999634355313174, 0.9999315506115275]
3.5255270584829996e-9

Juliaだけの例と, Fortranと連携した例で全く同じ結果が得られました.
https://github.com/ohno/RunningExternalPrograms

Discussion