はじめに
以前 PyCall を使って Ruby でハートを描く という記事を書きました。それから約 4 年後、久しぶりに PyCall を触ってみました。今度は 3D のハートを描画してみようと思います ❤️
PyCall について
PyCall は Ruby から Python のコードを呼び出すことができる、mrkn (Kenta Murata) さんが作成されている Gem です。この mrkn さんは numpy.rb や matplotlib.rb など、Python で非常に有名なライブラリのラッパーライブラリも作成してくれています。今回はそれらも使用します。
バージョン情報
$ ruby -v
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin20]
$ gem list | grep -e "pycall" -e "numpy" -e "matplotlib"
matplotlib (1.1.0)
numpy (0.2.0)
pycall (1.3.1)
$ python --version
Python 3.9.2
$ pip list | grep -e "numpy" -e "matplotlib"
matplotlib 3.4.1
numpy 1.20.2
コード
Stack Overflow の how to draw a heart with pylab という記事に記載されている Python のコードを Ruby に書き換えました。
require 'matplotlib/pyplot'
require 'numpy'
Axes3D = PyCall.import_module('mpl_toolkits.mplot3d').Axes3D
def calculate_3d_heart(x, y, z)
(x**2 + (9.0 / 4) * y**2 + z**2 - 1)**3 - x**2 * z**3 - (9.0 / 80) * y**2 * z**3
end
def plot_3d_heart
xmin, xmax, ymin, ymax, zmin, zmax = [-1.5, 1.5] * 3
fig = Matplotlib::Pyplot.figure
ax = fig.add_subplot(111, projection: '3d')
a = Numpy.linspace(xmin, xmax, 100)
b = Numpy.linspace(xmin, xmax, 40)
a1, a2 = Numpy.meshgrid(a, a)
# numpy.ndarray は each や to_a ができないので index で値にアクセスする。
b.size.times do |i|
x = a1
y = a2
z1 = b[i]
z2 = calculate_3d_heart(x, y, z1)
ax.contour(x, y, z1 + z2, [z1], zdir: 'z', colors: ['red'])
end
b.size.times do |i|
x = a1
z = a2
y1 = b[i]
y2 = calculate_3d_heart(x, y1, z)
ax.contour(x, y1 + y2, z, [y1], zdir: 'y', colors: ['red'])
end
b.size.times do |i|
y = a1
z = a2
x1 = b[i]
x2 = calculate_3d_heart(x1, y, z)
ax.contour(x1 + x2, y, z, [x1], zdir: 'x', colors: ['red'])
end
ax.set_zlim3d(zmin, zmax)
ax.set_xlim3d(xmin, xmax)
ax.set_ylim3d(ymin, ymax)
Matplotlib::Pyplot.show
end
plot_3d_heart
上記の Ruby スクリプトを実行すると Tk のウィンドウが開きます。
若干血しぶきが飛び散っているのはご愛嬌 😇
ちょっとした悩み
Python ではスカラー値を x、ベクトル値を X のように変数名の大文字と小文字を使い分けているが、Ruby ではどうすればいいのでしょう 🤔 Ruby だと大文字は定数と判断されるためです。