Fortran 2018をPythonから使う ― NumPy 2.5.4のf2pyとMesonで数値計算を組み込む
はじめに
Pythonはデータの読み込みや加工、グラフの作成、Web APIの開発に便利です。一方、河川の流れや氾濫、降雨流出のような数値計算では、Fortranで書かれたプログラムを使う機会が今でも多くあります。
既存のFortranプログラムをPythonで書き直す必要はありません。計算部分はFortranのまま使い、入力データの準備や計算結果の整理をPythonに任せる方法があります。その橋渡しをするのが、NumPyに付属する**f2py(Fortran to Python interface generator)**です。
今回は、2026年10月15日時点で公開されているNumPy 2.5.4を対象に、WSL2のUbuntu 24.04で使う手順をまとめます。簡単な配列計算から始め、2次元メッシュの計算、インターフェースの定義、Fortran 2018との付き合い方、最適化、Docker環境まで取り上げます。
f2pyで気を付けたいのは、最近のバージョンでビルド方法が変わっていることです。以前の記事にあるnumpy.distutils前提の手順は、現在のNumPyではそのまま使えない場合があります。
1. f2pyでできること
f2pyはFortranのサブルーチンや関数を解析し、Pythonから呼び出すための拡張モジュールを生成します。NumPy配列をFortranの配列引数に渡せるため、数値計算で使いやすい仕組みです。
例えば、時間積分やセルごとの更新をFortranで計算し、Python側ではNumPy配列の作成やGeoTIFFの読み書き、FastAPIとの連携を担当する構成にできます。
ただし、f2pyを使うだけで処理が高速化するわけではありません。単純な配列加算ならNumPyだけで十分に高速です。複雑なループや繰り返し計算をFortranにまとめて渡すと、PythonとFortranを往復する回数を減らせます。
NumPy 2.5で変わった点
NumPy 1.26系でMesonを使うビルド方法が導入され、NumPy 2.5では従来のdistutilsバックエンドが削除されました。現在はMesonを使ってビルドします。
| 項目 | 現在の扱い |
|---|---|
| f2py本体 | NumPyに同梱されています |
| ビルド | Mesonを使用します |
| Fortranコンパイラ | gfortranなどを使用します |
| Python側のデータ | 主にNumPy配列を使用します |
| インターフェース | ソースから自動生成するか、.pyfで定義します |
| Fortran 2018 | 計算内部で使用できますが、すべての構文を自動公開できるわけではありません |
基本的なビルドコマンドは次の形です。
python -m numpy.f2py -c -m fastcalc add_vectors.f90
-cがコンパイル、-m fastcalcがPythonに読み込むモジュール名です。python -m numpy.f2pyと書いておくと、使用中のPython環境にインストールされたNumPyを明示的に使えます。
2. WSL2 Ubuntu 24.04に環境を作る
想定する環境はWindows 11、WSL2 Ubuntu 24.04、Python 3.12、gfortranです。Windows側のVS CodeからWSL2のフォルダーを開いて編集しても構いません。
まず、システム側のツールをインストールします。
sudo apt update
sudo apt install -y \
python3 python3-venv python3-dev \
build-essential gfortran ninja-build pkg-config
次に、Pythonの仮想環境を作成します。
mkdir -p ~/f2py_project
cd ~/f2py_project
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install numpy==2.5.4 meson pytest
確認用のコマンドです。
python --version
python -c "import numpy; print(numpy.__version__)"
python -m numpy.f2py -v
gfortran --version
meson --version
ninja --version
ここではNumPyのバージョンを固定しています。ほかのNumPy 2.xを利用する場合も、実際に使うバージョンの公式マニュアルを確認してください。
3. 最初の例:2つの配列を加算する
まずは、2つの配列を足し合わせるサブルーチンをPythonから呼び出します。
作業ディレクトリは次の構成です。
f2py_project/
├── .venv/
├── add_vectors.f90
└── test_add.py
3.1 Fortranのコード
add_vectors.f90を作成します。
subroutine add_vectors(n, a, b, c)
implicit none
integer, intent(in) :: n
double precision, intent(in) :: a(n), b(n)
double precision, intent(out) :: c(n)
integer :: i
do concurrent (i = 1:n)
c(i) = a(i) + b(i)
end do
end subroutine add_vectors
do concurrentはFortran 2008で導入された構文で、Fortran 2018でも利用できます。ここでは各要素の計算が独立していることを示しています。ただし、これを書いただけで必ずSIMD化やマルチスレッド化が行われるわけではありません。
intent(in)は入力、intent(out)は出力です。f2pyはこれらの宣言を読み取り、Python向けの引数や戻り値を用意します。
3.2 拡張モジュールを作る
同じフォルダーで次のコマンドを実行します。
python -m numpy.f2py -c -m fastcalc add_vectors.f90
成功すると、Linuxでは次のような名前のファイルができます。PythonのバージョンやCPU環境によって実際のファイル名は変わります。
fastcalc.cpython-312-x86_64-linux-gnu.so
3.3 Pythonから呼び出す
test_add.pyを作成します。
"""Fortranの配列加算をPythonから呼び出す例。"""
import numpy as np
import fastcalc
def main() -> None:
"""配列を渡して、計算結果をNumPyで検証する。"""
a = np.array([1.0, 2.0, 3.0], dtype=np.float64)
b = np.array([10.0, 20.0, 30.0], dtype=np.float64)
c = fastcalc.add_vectors(a, b)
print("a =", a)
print("b =", b)
print("c =", c)
np.testing.assert_allclose(c, a + b)
if __name__ == "__main__":
main()
実行します。
python test_add.py
期待する結果は次のとおりです。
a = [1. 2. 3.]
b = [10. 20. 30.]
c = [11. 22. 33.]
Fortran側の引数には配列長nがありますが、Pythonでは指定していません。f2pyが配列の大きさを取得して渡せるためです。
生成された引数の説明は、Pythonから確認できます。
python -c "import fastcalc; print(fastcalc.add_vectors.__doc__)"
f2pyがどのような引数を公開したか迷ったときは、まずこの説明を見ると分かりやすいです。
4. intentとNumPy配列の受け渡し
Fortranの引数宣言は、Pythonからの使い方にも影響します。
| Fortranの宣言 | Python側での扱い |
|---|---|
intent(in) |
入力として渡します |
intent(out) |
多くの場合、戻り値として受け取ります |
intent(inout) |
入出力用の配列として渡します |
dimension(n) |
1次元配列として扱います |
dimension(nx,ny) |
2次元配列として扱います |
double precision |
通常はnp.float64に対応します |
はじめのうちは、入力と出力を別々にしておく方が実装しやすいと思います。intent(inout)の場合、型やメモリ配置によってコピーや書き戻しが関係するためです。
特に2次元以上の配列では、NumPyとFortranのメモリ配置の違いを意識する必要があります。
5. 2次元メッシュをFortranで計算する
もう少し実際の数値計算に近い例として、2次元の拡散方程式を1ステップだけ計算します。
\frac{\partial h}{\partial t}
=D\left(\frac{\partial^2 h}{\partial x^2}
+\frac{\partial^2 h}{\partial y^2}\right)
ここでは格子間隔を等しくした5点差分を使います。これはf2pyの配列受け渡しを確かめるための例であり、洪水氾濫の浅水流方程式ではありません。
5.1 Fortran側の計算処理
diffusion.f90を作成します。
diffusion.f90(クリックして開く)
subroutine diffusion_step(nx, ny, dt, dx, diffusivity, h, hnew)
implicit none
! メッシュ数と計算条件
integer, intent(in) :: nx, ny
double precision, intent(in) :: dt, dx, diffusivity
! 現在時刻と次時刻の配列
double precision, intent(in) :: h(nx, ny)
double precision, intent(out) :: hnew(nx, ny)
integer :: i, j
! 境界セルは現在の値を維持する
hnew = h
! 内部セルを5点差分で更新する
do concurrent (j = 2:ny-1, i = 2:nx-1)
hnew(i, j) = h(i, j) &
+ dt * diffusivity / (dx * dx) * ( &
h(i-1, j) + h(i+1, j) &
+ h(i, j-1) + h(i, j+1) &
- 4.0d0 * h(i, j) )
end do
end subroutine diffusion_step
拡散係数をD、時間刻みをdt、格子間隔をdxとすると、この陽解法の安定条件は、等間隔格子の場合、おおむね次のようになります。
\frac{D\,\Delta t}{\Delta x^2}\leq\frac{1}{4}
以下の例ではD=0.1、dt=1.0、dx=1.0なので、この条件を満たします。
5.2 2つのFortranソースをまとめてビルド
python -m numpy.f2py -c -m fastcalc \
add_vectors.f90 diffusion.f90
5.3 Pythonから計算する
test_diffusion.pyを作成します。
test_diffusion.py(クリックして開く)
"""2次元拡散方程式の1ステップ計算を検証する。"""
import numpy as np
import fastcalc
def main() -> None:
"""初期値を設定し、Fortranの計算結果を確認する。"""
nx, ny = 100, 100
dt = 1.0
dx = 1.0
diffusivity = 0.1
# Fortranと同じ列優先のメモリ配置にする
h = np.zeros((nx, ny), dtype=np.float64, order="F")
h[nx // 2, ny // 2] = 1.0
hnew = fastcalc.diffusion_step(
dt, dx, diffusivity, h
)
print("配列の形状:", hnew.shape)
print("中央セル:", hnew[nx // 2, ny // 2])
print("総和:", hnew.sum())
assert hnew.shape == (nx, ny)
np.testing.assert_allclose(
hnew[nx // 2, ny // 2], 0.6, atol=1.0e-12
)
np.testing.assert_allclose(
hnew.sum(), 1.0, atol=1.0e-12
)
if __name__ == "__main__":
main()
python test_diffusion.py
今回の初期条件では、中央セルは0.6、配列全体の総和は1.0になります。中央セルの周囲4セルにそれぞれ0.1ずつ移るためです。
なお、境界セルを固定するこの単純な処理が、どのような条件でも総量を保存するわけではありません。保存性を要求する実際の水理計算では、フラックスや境界条件を別途検証する必要があります。
6. C-orderとF-orderの違い
NumPyで通常作る2次元配列はC-order(行優先)です。一方、Fortranの配列はF-order(列優先)になっています。
# 一般的なNumPy配列
h_c = np.zeros((100, 100), dtype=np.float64)
# Fortranへの受け渡しを意識した配列
h_f = np.zeros((100, 100), dtype=np.float64, order="F")
# 既存配列を必要に応じて変換する
h_f2 = np.asfortranarray(h_c)
print(h_c.flags.f_contiguous)
print(h_f.flags.f_contiguous)
Fortranでは先頭の添字が連続するため、通常は次のループ順序がメモリアクセスに適しています。
do j = 1, ny
do i = 1, nx
h(i, j) = h(i, j) + 1.0d0
end do
end do
f2pyは必要に応じてメモリ配置を変換してくれますが、変換のためにコピーが発生することがあります。例えば、1000×1000の倍精度配列は約8 MBです。これを時間ステップごとに何枚もコピーすると、計算以外の処理時間が増えてしまいます。
コピーが発生するかを調べたいときは、f2pyの配列コピーレポート機能も使えます。詳細は公式のUsing F2PYにあります。
また、GeoTIFFなどのラスターデータでは、配列の(行, 列)とモデルの(x, y)の対応にも注意します。F-orderに変更することと、座標の向きを入れ替えることは別です。
7. .pyfファイルで公開する関数を選ぶ
実務のFortranプログラムには、内部計算用のサブルーチンがたくさんあります。それらをすべてPythonに公開する必要はありません。
f2pyのシグネチャファイル.pyfを使うと、公開する関数や引数を明示できます。
まず、インターフェースのひな型を生成します。
python -m numpy.f2py -h fastcalc.pyf -m fastcalc \
add_vectors.f90 diffusion.f90
例えば、配列加算のみを公開する場合のfastcalc.pyfは次のように書けます。
! -*- f90 -*-
python module fastcalc
interface
subroutine add_vectors(n, a, b, c)
integer intent(hide), depend(a) :: n = shape(a, 0)
double precision dimension(n), intent(in) :: a
double precision dimension(n), intent(in) :: b
double precision dimension(n), intent(out) :: c
end subroutine add_vectors
end interface
end python module fastcalc
ビルドします。
python -m numpy.f2py -c fastcalc.pyf add_vectors.f90
.pyfにモジュール名が定義されているので、この場合は-mを付けません。既存のソルバーにPython用の入口を設けるときには、この方法が便利です。
8. Fortran 2018の機能を使うときの注意点
Fortran 2018で書いたコードをgfortranがコンパイルできることと、f2pyがその構文を理解してPython向けのインターフェースを自動生成できることは別です。
| 機能 | 考え方 |
|---|---|
| 通常のサブルーチン・関数 | f2pyで扱いやすい構成です |
数値配列・intent
|
主な用途として対応しています |
do concurrent |
Fortran側の計算内部で使えます |
real(real64) |
KINDの型マッピングを確認します |
allocatable配列 |
公開の仕方に制約があります |
派生型typeや多態性class
|
そのまま自動公開できない場合があります |
submodule、Coarray |
計算内部で使い、境界は単純なAPIにする方が確実です |
real(real64)の型指定
Fortran 2018では、次のように精度を明示する書き方を使えます。
use, intrinsic :: iso_fortran_env, only: real64
real(real64) :: water_level
f2py側でこのKINDを意図どおりに認識できない場合は、.f2py_f2cmapファイルを用意します。
dict(real=dict(real64="double"))
この対応付けはNumPyの公式ドキュメントにも掲載されています。複雑な派生型まで自動で扱えるようになる仕組みではないため、公開APIには単純な実数・整数・配列を使う方が管理しやすいです。
ソルバー本体と公開APIを分離する
例えば河川計算ソルバーなら、次のような構成です。
hydraulic_solver/
├── src/
│ ├── continuity.f90
│ ├── momentum.f90
│ ├── boundary.f90
│ └── hydraulic_api.f90
├── interface/
│ └── hydraulic_api.pyf
└── python/
├── run_solver.py
└── test_solver.py
Fortran内部ではモジュールや派生型を利用しても構いません。Pythonとの境界にあるhydraulic_api.f90では、配列と基本データ型だけを受け渡すようにします。既存の計算コアを大きく変更せずに済み、Fortran単独での試験も続けられます。
9. 高速化するときの考え方
f2pyはPythonとFortranを接続するツールであり、数値計算そのものの高速化はFortran側の実装やコンパイルオプションが担当します。
コンパイラの最適化
例えば、gfortranで最適化を有効にしてビルドするなら、次のように指定できます。
FC=gfortran \
FFLAGS="-O3 -march=native -std=f2018" \
python -m numpy.f2py -c -m fastcalc \
add_vectors.f90 diffusion.f90
-O3は積極的な最適化、-march=nativeはビルドするCPUに合わせた命令の利用です。-march=nativeで作ったバイナリを別のCPUへ配布する場合は注意してください。
ベクトル化の状況を確認したいときは、-fopt-info-vec-optimizedによるコンパイラの診断メッセージも参考になります。
水理計算では、計算速度だけでなく水収支や再現性が重要です。浮動小数点演算の意味が変わり得る-ffast-mathは、検証が済むまで安易に使わない方がよいと思います。
SIMDとOpenMP
配列の連続領域を更新するループはSIMD化の候補になります。また、セル同士が独立している処理はOpenMPによる並列化も検討できます。
!$omp parallel do private(i,j) schedule(static)
do j = 1, ny
do i = 1, nx
hnew(i,j) = h(i,j) + source(i,j)
end do
end do
!$omp end parallel do
OpenMPを使う場合は、コンパイル時だけでなくリンク時にも必要な設定を行います。複数のFortranファイルや外部ライブラリがある場合は、Meson側で依存関係を明示した構成にすると管理しやすくなります。
ただし、共有変数やsave変数を扱うコードは、並列実行時のデータ競合に注意が必要です。
PythonとFortranを何度も往復しない
大量のセルを計算する場合、例えば次のような違いがあります。
避けたい例
Python: 1セル分をFortranに渡す → 戻す → 次のセルを渡す → …
改善例
Python: メッシュ全体を渡す
Fortran: 全セル・複数時間ステップをまとめて計算
Python: 必要な結果を受け取る
高速化を考えるときは、Fortranのループだけでなく、配列のコピー量とPythonからの呼び出し回数も確認する必要があります。
10. Dockerで実行環境を揃える
同じ構成をDockerでも利用できます。以下は配列加算と2次元拡散をコンテナ内でビルドする例です。
Dockerfile(クリックして開く)
FROM python:3.12-slim
# C / FortranコンパイラとMesonの実行に必要なツール
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
gfortran \
ninja-build \
pkg-config && \
rm -rf /var/lib/apt/lists/*
# f2py本体はNumPyに含まれる
RUN python -m pip install --no-cache-dir \
numpy==2.5.4 \
meson \
pytest
WORKDIR /work
COPY add_vectors.f90 diffusion.f90 test_diffusion.py ./
RUN python -m numpy.f2py -c -m fastcalc \
add_vectors.f90 diffusion.f90
CMD ["python", "test_diffusion.py"]
docker build -t f2py-demo .
docker run --rm f2py-demo
Dockerfileは実装例です。Pythonパッケージのバージョンだけでなく、コンパイラや依存ライブラリも固定したい場合は、ベースイメージのダイジェストやパッケージのバージョンも管理します。
11. エラーが出たときに確認すること
| 症状 | 確認すること |
|---|---|
No module named numpy |
仮想環境が有効か、NumPyが入っているか |
No module named mesonbuild |
python -m pip install mesonを実行したか |
| Fortranコンパイラが見つからない |
gfortran --versionとFCの設定 |
No module named fastcalc |
.soの生成場所と実行するPython環境 |
| 配列の型・次元に関するエラー |
dtype、shape、order、引数の順序 |
undefined symbol |
リンク対象のオブジェクトやライブラリ |
| Fortranの構文解析に失敗する |
.pyfでAPIを限定できないか |
f2pyの公開関数を確認するには、次のコマンドが便利です。
python -c "import fastcalc; print(fastcalc.__doc__)"
また、NumPy 2.xでは以前のnumpy.f2py.compile()が削除されています。Pythonスクリプトからビルドを呼ぶ場合は、現在のコマンドラインツールをsubprocess.run()などで起動する形に変更します。
12. 動作確認で大切にしたいこと
今回の2つのFortranプログラムは、gfortran 14.2.0でFortran 2018としてコンパイルし、配列加算と2次元拡散の計算値を確認しています。また、NumPy 2.3.5のf2pyで.pyfを生成・解析できることも確認しています。
一方、NumPy 2.5.4とMesonを使った拡張モジュールの最終ビルド、およびPythonからの実行までは、この検証環境では実施していません。掲載したDockerfileについても、コンテナをビルドした結果を示すものではありません。環境を構築したら、記事中のテストを実行して確認してください。
河川・氾濫計算のようなプログラムでは、結果が返ってくることだけでなく、次の項目も確認しておくべきです。
- Fortran単独実行とf2py経由で、同じ入力に対して計算結果が一致するか
- 配列の形状、座標軸、メモリ配置が想定どおりか
- 水位、流量、水収支などの物理量に不自然な差が生じていないか
- 配列コピーやPythonとの往復に、どの程度の時間を使っているか
- OpenMPや最適化オプションを変えても、許容できる計算精度が保たれるか
おわりに
f2pyを使うと、Fortranで作った数値計算プログラムをPythonから比較的少ない変更で利用できます。
特に既存の計算ソルバーがある場合、まずは簡単なサブルーチンを公開して配列の受け渡しを確認し、次に時間積分などの計算処理をまとまった単位で呼び出す形にすると進めやすくなります。
Fortran 2018の機能を全面的にf2pyへ見せる必要はありません。計算コアとPython向けAPIを分けておけば、Fortran側の実装を維持しながら、Pythonのデータ処理、可視化、Web APIなどと組み合わせることができます。
次の段階では、1次元河道や2次元メッシュの時間積分をFortran側にまとめ、Fortran単独実行とf2py経由で計算結果・水収支・処理時間を比較してみたいと思います。
参考資料
- NumPy公式:F2PY user guide and reference manual
- NumPy公式:Using F2PY
- NumPy公式:F2PY and build systems
- NumPy公式:Migrating to Meson
- NumPy公式:Advanced F2PY use cases
- PyPI:NumPy 2.5.4
