Fortran で動的に AI にスクリプトを書かせて実行するデモ
自分でプログラムを考えるのが馬鹿らしいこの頃、Fortran ではめんどくさい作業を、自然言語のプロンプトを与えて、動的に AI に Python スクリプトを書いてもらい、それをそのまま実行することを考えます。
しかし、AI の反乱を恐れて生成されたスクリプトは隔離された環境で行うのが流行りのようなので、その波に乗ることにします。smolvm という独立 Kernel をもつ Virtual Machine を Docker に近い身軽さで起動するものが、AI 生成プログラムなどを動かす Sandbox 的隔離環境として流行っているようです。ところで smolvm は同名のものが複数あって、何が何やらわからないのですが、WSL2 の環境で動くものを AI に選ばせたところ pip で入る smolvm を指名されたので、それを用います。WSL2 のせいか色々制限があるので AI によく聞かれるのがいいと思います。
smolvm のデフォルトの vm は何も入ってない Alpine Linux の最小イメージなので、Python/numpy/matplotlib インストールしたイメージを一回作って保存し、そのイメージを保存して利用することにします。これはすこし太ったイメージなので VM 起動が少し遅くなります。
VM はメモリー的に完全に隔離されて WSL2 では、ReadOnly でも disk 共有ができなかったので、scp でコピーすることで対応します。結果を持ってくるにはこれを使わざるおえないので、まぁいいでしょう。
実行結果
以下では Fortran が Bessel 関数を計算し、バイナリファイルに配列データとして出力します。
次に、curl を使って、AI (GPT-5.4-mini) の API 呼び出しを行い、matplotlib で図を描くプログラムを作れと命じます。API 呼び出しには json が必要になりますが、Linux の jq コマンド利用で切り抜けます。ここで python スクリプトが出来上がります。OpenAI の key を環境変数にセットしておく必要があります。漏洩すると、悪いおじさんが乱用してとんでもない額の請求書がやってくるようです。
最後に、いま生成したスクリプトを smolvm で実行します。あらかじめ smolvm の設定が必要になります。なかなか面倒なので、AI に言われるまま設定する必要となります。動くまでだいぶ苦労しました。smolvm は完全独立したメモリ空間にあるので、データの受け渡しは工夫が必要です。ここでは scp によるファイルコピーで解決しました。初めは、Disk の共有で行こうとしたのですが、WSL2 では無理なようでした。ip アドレスのマスカレード処理もする必要がありましたが、AI の言うがままに行ったため説明できません。気分的にはエドガー・ポーの「アモンティリャードの酒樽」の最後のごとく「アモンティリャード」と繰り返すのみです。
なお、プログラムは主に ChatGPT と Claude の chat mode で作り、時々 Gemini にも聞きました。自力では smolvm 設定や API 呼び出しの json 処理などは到底なしえなかったと思います。

AI に与えたプロンプトと、その指示から生成したスクリプト
見た感じ、複数回実行しても毎回改行以外は同じ Python スクリプトが生成されています。
Read data.bin as raw binary float64 data using numpy.fromfile. The file contains repeated pairs x,y: x0,y0,x1,y1,... . Reshape the array to (-1, 2). Use column 0 as x and column 1 as y. Plot y vs x and save to plot.png. Use matplotlib with the gg backend. Do not use pandas. Output ONLY raw Python code, no fences, no explanation.
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
data = np.fromfile("data.bin", dtype=np.float64)
xy = data.reshape(-1, 2)
x = xy[:, 0]
y = xy[:, 1]
plt.plot(x, y)
plt.xlabel("x")
plt.ylabel("y")
plt.savefig("plot.png", dpi=150, bbox_inches="tight")
#プログラム
module ai_openai_mod
implicit none
private
public :: ai_generate_python
character(len=*), parameter :: DEFAULT_MODEL = "gpt-5.4-mini"
contains
subroutine ai_generate_python(prompt, outfile, model)
character(len=*), intent(in) :: prompt
character(len=*), intent(in) :: outfile
character(len=*), intent(in), optional :: model
character(len=:), allocatable :: m
integer :: u
m = DEFAULT_MODEL
if (present(model)) m = model
open(newunit=u, file="prompt.txt", status="replace", action="write")
write(u,'(a)') prompt
close(u)
call execute_command_line( &
'jq -n --arg model "' // m // '" --rawfile content prompt.txt ' // &
'''{model: $model, messages: [{role: "user", content: $content}]}''' // &
' > req.json')
call execute_command_line( &
'curl -sS https://api.openai.com/v1/chat/completions ' // &
'-H "Authorization: Bearer $OPENAI_API_KEY" ' // &
'-H "Content-Type: application/json" ' // &
'-d @req.json | jq -r ".choices[0].message.content" > ' // outfile)
end subroutine ai_generate_python
end module ai_openai_mod
program demo
use iso_fortran_env, only : real64
implicit none
integer :: u, i, ios
real(real64) :: x, y
character(len=*), parameter :: fbin = 'data.bin'
character(len=*), parameter :: pys = 'gen.py'
open(newunit=u, file=fbin, status='replace', action='write', &
access='stream', form='unformatted', iostat=ios)
if (ios /= 0) stop 'cannot open data.bin'
do i = 0, 200
x = 0.1_real64 * i
y = bessel_j0(x)
write(u) x, y
end do
close(u)
AI:block
use ai_openai_mod, only: ai_generate_python
character(len=*), parameter :: PROMPT = &
"Read data.bin as raw binary float64 data using numpy.fromfile. " // &
"The file contains repeated pairs x,y: x0,y0,x1,y1,... . " // &
"Reshape the array to (-1, 2). " // &
"Use column 0 as x and column 1 as y. " // &
"Plot y vs x and save to plot.png. " // &
"Use matplotlib with the Agg backend. Do not use pandas. " // &
"Output ONLY raw Python code, no fences, no explanation."
print *, 'Asking AI...'
call ai_generate_python(PROMPT, pys)
print *, '...Finished'
end block AI
smolvm:block
character(len=*), parameter :: vm = 'ftvm'
character(len=*), parameter :: sshkey = '~/.smolvm/keys/id_ed25519'
character(len=*), parameter :: sshopts = &
'-o StrictHostKeyChecking=no -o BatchMode=yes'
character(len=4096) :: cmd
character(len=16) :: port
integer :: exitstat
call execute_command_line( &
'smolvm stop ' // vm // ' >/dev/null 2>&1 || true', exitstat=exitstat)
call execute_command_line( &
'smolvm delete ' // vm // ' >/dev/null 2>&1 || true', exitstat=exitstat)
call execute_command_line( &
'cp ~/.smolvm/images/alpine-with-python.ext4' // &
' ~/.local/state/smolvm/disks/' // vm // '.ext4', &
exitstat=exitstat)
if (exitstat /= 0) stop 'image restore failed'
cmd = 'smolvm create --backend firecracker --name ' // vm // &
' --boot-timeout 30'
print *, trim(cmd)
call execute_command_line(trim(cmd), exitstat=exitstat)
if (exitstat /= 0) stop 'smolvm create failed'
call execute_command_line( &
'smolvm list --json | jq -r ''.data.vms[] | select(.name=="' // vm // &
'") | .ssh_port'' > .port_tmp', exitstat=exitstat)
if (exitstat /= 0) stop 'port detection failed'
open(newunit=u, file='.port_tmp', status='old', action='read', iostat=ios)
if (ios /= 0) stop 'cannot read port'
read(u,'(A)') port
port = adjustl(port)
close(u)
call execute_command_line('rm -f .port_tmp', exitstat=exitstat)
print *, 'SSH port: ', trim(port)
cmd = 'scp -i ' // sshkey // ' -P ' // trim(port) // ' ' // sshopts // &
' ' // fbin // ' ' // pys // ' root@127.0.0.1:/tmp/'
print *, trim(cmd)
call execute_command_line(trim(cmd), exitstat=exitstat)
if (exitstat /= 0) stop 'scp to vm failed'
cmd = 'ssh -i ' // sshkey // ' -p ' // trim(port) // ' ' // sshopts // &
' root@127.0.0.1' // &
' "cd /tmp && python3 ' // pys // '"'
print *, trim(cmd)
call execute_command_line(trim(cmd), exitstat=exitstat)
if (exitstat /= 0) stop 'python in vm failed'
cmd = 'scp -i ' // sshkey // ' -P ' // trim(port) // ' ' // sshopts // &
' root@127.0.0.1:/tmp/plot.png ./plot.png'
print *, trim(cmd)
call execute_command_line(trim(cmd), exitstat=exitstat)
if (exitstat /= 0) stop 'scp from vm failed'
call execute_command_line('smolvm stop ' // vm, exitstat=exitstat)
call execute_command_line( &
'smolvm delete ' // vm // ' >/dev/null 2>&1', exitstat=exitstat)
call execute_command_line('rm -f prompt.txt req.json .port_tmp', exitstat=exitstat)
end block smolvm
print *, 'done. host file: plot.png'
end program demo