この手の話は、DALL-E の頃から言われていて、何かと数学的な図を書かせようとするとどこからかのなんちゃって画像を持ってくるために変なことになります。たぶん、古い教科書のスキャン画像とかを学習データにいれてしまっていて、そこから引っ張ってきているだけです。そもそも、画像の生成 AI に関しては、「教師なし学習」での推論でしかなくて、数学や物理のような自然科学的な解答のある「教師あり学習」の結果を求めようとしても無駄です。このあたりは、漫画やイラストを描かせた場合にはなんとなくいいけれど、部分的に手の指がおかしかったり腕が三本あったりするのがそれです。このあたりの正確性≒正解と明確に分かるものは、将来的にAIエージェントによる自己チェック機能で避けることができると思います。
まあ、その自己チェックを入れてあげれば、この手の図も正確にできるようになると、という例が以下のものです。
現状の唯一の方法としては、先の「シュレディンガー方程式」については、直接画像AIを使うのではなく、いったん Python などを使って正確な式から正確な図を描くようにします。こうすると、当たり前ですが正確な図ができますね。これを、切り貼りする(素材として画像AIに渡してもいいでしょう)ことで、「数学的に正確な図」を私達は得ることができます。
当たり前といえば、当たり前なのですが、この手のなんちゃって画像AI全般に言えることで、プロンプトがどうという話ではなく、さきに書いた通り
・「教師なし学習」的な正解ではないもの、つまりは「創造性」みたいなものを求めるのか?
・「教師あり学習」的な数学や物理のように自然科学として正解があるものを求めるのか?
という違いです。先の X のポストから言えば、当然後者のほうではあるのですが、実は意図として「数学的には正しくはないけれど、なんとなく中世で扱っていた数学っぽい SF 的な図を作り出して」というプロンプトであれば、前者が求められるというわけです。
で、実験的に Python でコードを出力もらったらどうなのか? ということを試してみましょう。以下は「Claude Sonnet 4.5」を使って Python コードを出力したものです。画像は、matplotlib.pyplot を使って PNG 形式で得ることができます。
ランダム値
プロンプトで「ランダム値をプロットして」というと、これもなんちゃって画像が生成されます。実際にランダム値の正確な図が欲しいのであれば、Python コードで書けばいいだけです。さぼってはいけませんw
# 一様分布のグラフを描く
import numpy as np
import matplotlib.pyplot as plt
# 日本語フォントの設定
plt.rcParams['font.sans-serif'] = ['MS Gothic', 'Yu Gothic', 'Meiryo']
plt.rcParams['axes.unicode_minus'] = False
# 一様分布からランダム値を生成
np.random.seed(42) # 再現性のため
n_samples = 10000
# 0から1の範囲で一様分布
uniform_values = np.random.uniform(0, 1, n_samples)
# グラフの作成
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 1. ヒストグラム
axes[0, 0].hist(uniform_values, bins=50, edgecolor='black', alpha=0.7)
axes[0, 0].set_title('一様分布のヒストグラム')
axes[0, 0].set_xlabel('値')
axes[0, 0].set_ylabel('度数')
axes[0, 0].grid(True, alpha=0.3)
# 2. 累積分布
axes[0, 1].hist(uniform_values, bins=50, cumulative=True, edgecolor='black', alpha=0.7)
axes[0, 1].set_title('累積分布')
axes[0, 1].set_xlabel('値')
axes[0, 1].set_ylabel('累積度数')
axes[0, 1].grid(True, alpha=0.3)
# 3. 散布図(サンプル順)
sample_indices = np.arange(min(500, n_samples))
axes[1, 0].scatter(sample_indices, uniform_values[:len(sample_indices)], alpha=0.5, s=10)
axes[1, 0].set_title('ランダム値の分布(最初の500サンプル)')
axes[1, 0].set_xlabel('サンプル番号')
axes[1, 0].set_ylabel('値')
axes[1, 0].grid(True, alpha=0.3)
# 4. 理論値との比較
sorted_values = np.sort(uniform_values)
theoretical = np.linspace(0, 1, n_samples)
axes[1, 1].plot(theoretical, sorted_values, 'b-', alpha=0.5, label='実測値')
axes[1, 1].plot([0, 1], [0, 1], 'r--', label='理論値(y=x)')
axes[1, 1].set_title('Q-Qプロット(理論値との比較)')
axes[1, 1].set_xlabel('理論分位点')
axes[1, 1].set_ylabel('実測分位点')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('一様分布.png', dpi=300, bbox_inches='tight')
plt.show()
print(f'サンプル数: {n_samples}')
print(f'平均値: {np.mean(uniform_values):.4f} (理論値: 0.5000)')
print(f'標準偏差: {np.std(uniform_values):.4f} (理論値: {1/np.sqrt(12):.4f})')
print(f'最小値: {np.min(uniform_values):.4f}')
print(f'最大値: {np.max(uniform_values):.4f}')

波動関数
波動関数をグラフにしたものです、波動関数自体は数学的な式なので、画像AIに頼るよりも Python などで正確に描いたほうが良いです。ここぐらいまでは、コードやグラフを眺めて、ほぼ出ているだろうというレベルでしょう。
# シュレディンガー方程式の「波動関数」と「確率密度」
# 波動関数の例を 2D グラフで
# 確率密度のグラフを 3D グラフで
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# 日本語フォントの設定
plt.rcParams['font.sans-serif'] = ['MS Gothic', 'Yu Gothic', 'Meiryo']
plt.rcParams['axes.unicode_minus'] = False
# 1次元無限井戸型ポテンシャルの波動関数
def wave_function_1d(x, n, L):
"""
1次元無限井戸型ポテンシャルの波動関数
n: 量子数 (1, 2, 3, ...)
L: 井戸の幅
"""
return np.sqrt(2/L) * np.sin(n * np.pi * x / L)
# 2次元波動関数(例:水素原子の2p軌道)
def wave_function_2d(x, y):
"""
簡易的な2次元波動関数の例
"""
r = np.sqrt(x**2 + y**2)
return r * np.exp(-r) * np.cos(np.arctan2(y, x))
# 確率密度関数(波動関数の絶対値の2乗)
def probability_density(psi):
"""
確率密度 = |ψ|²
"""
return np.abs(psi)**2
# ===== 1. 波動関数の2Dグラフ =====
fig = plt.figure(figsize=(16, 10))
# 1次元波動関数(複数の量子状態)
x = np.linspace(0, 1, 1000)
L = 1.0
ax1 = plt.subplot(2, 3, 1)
for n in [1, 2, 3, 4]:
psi = wave_function_1d(x, n, L)
ax1.plot(x, psi, label=f'n={n}')
ax1.set_xlabel('位置 x')
ax1.set_ylabel('波動関数 ψ(x)')
ax1.set_title('1次元無限井戸型ポテンシャルの波動関数')
ax1.legend()
ax1.grid(True, alpha=0.3)
ax1.axhline(y=0, color='k', linestyle='-', linewidth=0.5)
# 確率密度(1次元)
ax2 = plt.subplot(2, 3, 2)
for n in [1, 2, 3, 4]:
psi = wave_function_1d(x, n, L)
prob = probability_density(psi)
ax2.plot(x, prob, label=f'n={n}')
ax2.set_xlabel('位置 x')
ax2.set_ylabel('確率密度 |ψ(x)|²')
ax2.set_title('確率密度(1次元)')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 波動関数の実部と虚部(時間発展を含む例)
ax3 = plt.subplot(2, 3, 3)
t = 0
n = 2
psi = wave_function_1d(x, n, L)
E = n**2 # エネルギー固有値(簡略化)
psi_real = psi * np.cos(E * t)
psi_imag = psi * np.sin(E * t)
ax3.plot(x, psi_real, label='実部 Re(ψ)', color='blue')
ax3.plot(x, psi_imag, label='虚部 Im(ψ)', color='red')
ax3.plot(x, np.abs(psi), label='振幅 |ψ|', color='green', linestyle='--')
ax3.set_xlabel('位置 x')
ax3.set_ylabel('波動関数')
ax3.set_title(f'波動関数の実部・虚部(n={n}, t={t})')
ax3.legend()
ax3.grid(True, alpha=0.3)
ax3.axhline(y=0, color='k', linestyle='-', linewidth=0.5)
# ===== 2. 2次元確率密度の3Dグラフ =====
# 2次元グリッド
x_2d = np.linspace(-5, 5, 100)
y_2d = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x_2d, y_2d)
# 波動関数を計算
psi_2d = wave_function_2d(X, Y)
prob_2d = probability_density(psi_2d)
# 3Dプロット
ax4 = plt.subplot(2, 3, 4, projection='3d')
surf = ax4.plot_surface(X, Y, prob_2d, cmap='viridis', alpha=0.8)
ax4.set_xlabel('x')
ax4.set_ylabel('y')
ax4.set_zlabel('確率密度 |ψ|²')
ax4.set_title('2次元確率密度(3D表示)')
plt.colorbar(surf, ax=ax4, shrink=0.5)
# 2Dヒートマップ(上から見た図)
ax5 = plt.subplot(2, 3, 5)
contour = ax5.contourf(X, Y, prob_2d, levels=20, cmap='viridis')
ax5.set_xlabel('x')
ax5.set_ylabel('y')
ax5.set_title('確率密度(ヒートマップ)')
ax5.set_aspect('equal')
plt.colorbar(contour, ax=ax5)
# 等高線プロット
ax6 = plt.subplot(2, 3, 6)
contour_lines = ax6.contour(X, Y, prob_2d, levels=15, colors='black', linewidths=0.5)
ax6.contourf(X, Y, prob_2d, levels=20, cmap='plasma', alpha=0.7)
ax6.clabel(contour_lines, inline=True, fontsize=8)
ax6.set_xlabel('x')
ax6.set_ylabel('y')
ax6.set_title('確率密度(等高線)')
ax6.set_aspect('equal')
plt.tight_layout()
plt.savefig('波動関数と確率密度.png', dpi=300, bbox_inches='tight')
plt.show()
# 統計情報を出力
print("=" * 50)
print("1次元波動関数の統計(n=1の場合)")
print("=" * 50)
psi_1 = wave_function_1d(x, 1, L)
prob_1 = probability_density(psi_1)
print(f"波動関数の最大値: {np.max(np.abs(psi_1)):.4f}")
print(f"確率密度の積分(規格化確認): {np.trapz(prob_1, x):.4f}")
print(f"期待値 <x>: {np.trapz(x * prob_1, x):.4f}")
print("\n" + "=" * 50)
print("2次元確率密度の統計")
print("=" * 50)
total_prob = np.sum(prob_2d) * (x_2d[1] - x_2d[0]) * (y_2d[1] - y_2d[0])
print(f"確率密度の総和(近似): {total_prob:.4f}")
print(f"最大確率密度: {np.max(prob_2d):.6f}")
max_idx = np.unravel_index(np.argmax(prob_2d), prob_2d.shape)
print(f"最大確率密度の位置: (x={X[max_idx]:.2f}, y={Y[max_idx]:.2f})")

水素原子の電子軌道
実は、水素原子の軌道計算がちょっとアヤシイです。自分の場合は、原子力学科なのでこの軌道計算は最初の頃にやった筈なのですが(まあ、波動関数自体はいわゆる常識の範囲なので)、図の書き方が一般的に流通するものと違うのでなんとも言えないです。
ただし、電子軌道計算は、https://betterlate-thannever.github.io/Chemistry-2e/%E7%AC%AC6%E7%AB%A0-%E5%85%83%E7%B4%A0%E3%81%AE%E9%9B%BB%E5%AD%90%E6%A7%8B%E9%80%A0%E3%81%A8%E5%91%A8%E6%9C%9F%E7%9A%84%E6%80%A7%E8%B3%AA.html#%E9%87%8F%E5%AD%90%E8%AB%96%E3%81%AE%E7%99%BA%E5%B1%95 にあるように描かれるのが定番ではあるのですが、実際には電子雲の確率として示されるので、ちょっと旧来の書き方はいわゆる電子の粒のイメージが強くて、微妙なんですよね。確かに、私も「ダンベル型」になるという覚えがあるのですが、このイメージよりも単純な電子雲つまり数式そのものとしてはあくするようないい気がしています。まあ、どちらにせよ、模式図でしかないので、計算するとこうなるよという Python コードと図です。
# 水素原子の電子軌道の 3D グラフ
# s、p、f、d 軌道も追加
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.special import sph_harm_y, genlaguerre, factorial
import matplotlib.colors as mcolors
# 日本語フォントの設定
plt.rcParams['font.sans-serif'] = ['MS Gothic', 'Yu Gothic', 'Meiryo']
plt.rcParams['axes.unicode_minus'] = False
# ボーア半径(原子単位)
a0 = 1.0
def radial_wave_function(r, n, l):
"""
動径波動関数 R_nl(r)
n: 主量子数
l: 軌道角運動量量子数
"""
rho = 2 * r / (n * a0)
norm = np.sqrt((2 / (n * a0))**3 * factorial(n - l - 1) / (2 * n * factorial(n + l)))
laguerre = genlaguerre(n - l - 1, 2 * l + 1)(rho)
return norm * np.exp(-rho / 2) * rho**l * laguerre
def hydrogen_orbital(r, theta, phi, n, l, m):
"""
水素原子の波動関数 ψ_nlm(r, θ, φ)
n: 主量子数 (1, 2, 3, ...)
l: 軌道角運動量量子数 (0, 1, ..., n-1)
m: 磁気量子数 (-l, ..., 0, ..., l)
"""
R_nl = radial_wave_function(r, n, l)
Y_lm = sph_harm_y(l, m, theta, phi)
return R_nl * Y_lm
def create_orbital_visualization(n, l, m, resolution=50, r_max=None):
"""
軌道を3D可視化
"""
if r_max is None:
r_max = n**2 * a0 * 3 # 適切な範囲を設定
# 球座標グリッド
theta = np.linspace(0, np.pi, resolution)
phi = np.linspace(0, 2*np.pi, resolution)
THETA, PHI = np.meshgrid(theta, phi)
# 確率密度の最大値を探すための半径
r_values = np.linspace(0.1, r_max, 100)
max_prob = 0
optimal_r = r_max / 2
for r_test in r_values:
psi = hydrogen_orbital(r_test, np.pi/2, 0, n, l, m)
prob = np.abs(psi)**2 * r_test**2
if prob > max_prob:
max_prob = prob
optimal_r = r_test
# 複数の半径で等値面をプロット
r_surfaces = [optimal_r * factor for factor in [0.5, 0.8, 1.0]]
return THETA, PHI, r_surfaces
# 各軌道の名前
orbital_names = {
(1, 0, 0): '1s',
(2, 0, 0): '2s',
(2, 1, -1): '2p_y',
(2, 1, 0): '2p_z',
(2, 1, 1): '2p_x',
(3, 0, 0): '3s',
(3, 1, -1): '3p_y',
(3, 1, 0): '3p_z',
(3, 1, 1): '3p_x',
(3, 2, -2): '3d_xy',
(3, 2, -1): '3d_yz',
(3, 2, 0): '3d_z²',
(3, 2, 1): '3d_xz',
(3, 2, 2): '3d_x²-y²',
(4, 0, 0): '4s',
(4, 1, 0): '4p_z',
(4, 2, 0): '4d_z²',
(4, 3, -3): '4f_y(3x²-y²)',
(4, 3, -2): '4f_xyz',
(4, 3, -1): '4f_yz²',
(4, 3, 0): '4f_z³',
(4, 3, 1): '4f_xz²',
(4, 3, 2): '4f_z(x²-y²)',
(4, 3, 3): '4f_x(x²-3y²)',
}
# 可視化する軌道(s, p, d, f 軌道を含む)
orbitals_to_plot = [
(1, 0, 0), # 1s
(2, 1, 1), # 2px
(3, 2, 1), # 3dxz
(4, 3, 1), # 4fxz²
]
fig = plt.figure(figsize=(20, 16))
for idx, (n, l, m) in enumerate(orbitals_to_plot, 1):
ax = fig.add_subplot(3, 5, idx, projection='3d')
THETA, PHI, r_surfaces = create_orbital_visualization(n, l, m, resolution=60)
# 各半径で等値面をプロット
for i, r in enumerate(r_surfaces):
# 直交座標に変換
X = r * np.sin(THETA) * np.cos(PHI)
Y = r * np.sin(THETA) * np.sin(PHI)
Z = r * np.cos(THETA)
# 波動関数を計算
psi = hydrogen_orbital(r, THETA, PHI, n, l, m)
# 確率密度(実部の符号で色分け)
prob = np.abs(psi)**2
phase = np.angle(psi)
colors = np.real(psi)
# 正負で色分け(vmin < vcenter < vmax を保証)
vmin, vmax = colors.min(), colors.max()
if vmin >= 0:
vmin = -1e-10
if vmax <= 0:
vmax = 1e-10
norm = mcolors.TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
surf = ax.plot_surface(X, Y, Z, facecolors=plt.cm.RdBu(norm(colors)),
alpha=0.7 - i*0.2, shade=True,
linewidth=0, antialiased=True)
# 軸設定
orbital_name = orbital_names.get((n, l, m), f'{n},{l},{m}')
ax.set_title(f'{orbital_name} (n={n}, l={l}, m={m})', fontsize=12, fontweight='bold')
ax.set_xlabel('x (a₀)')
ax.set_ylabel('y (a₀)')
ax.set_zlabel('z (a₀)')
# 範囲を統一
max_range = n**2 * a0 * 2
ax.set_xlim(-max_range, max_range)
ax.set_ylim(-max_range, max_range)
ax.set_zlim(-max_range, max_range)
# 視点を調整
ax.view_init(elev=20, azim=45)
# グリッドを薄く
ax.grid(True, alpha=0.2)
plt.tight_layout()
plt.savefig('水素原子の電子軌道.png', dpi=300, bbox_inches='tight')
plt.show()
# 軌道の特徴を出力
print("=" * 60)
print("水素原子の電子軌道の特徴")
print("=" * 60)
for n, l, m in orbitals_to_plot:
orbital_name = orbital_names.get((n, l, m), f'{n},{l},{m}')
# 動径波動関数の最大値の位置(最も確率の高い半径)
r_values = np.linspace(0.01, n**2 * a0 * 3, 1000)
radial_prob = [radial_wave_function(r, n, l)**2 * r**2 for r in r_values]
max_idx = np.argmax(radial_prob)
r_max_prob = r_values[max_idx]
print(f"\n{orbital_name}軌道:")
print(f" 主量子数 n = {n}")
print(f" 角運動量量子数 l = {l} ({'s' if l==0 else 'p' if l==1 else 'd' if l==2 else 'f'}軌道)")
print(f" 磁気量子数 m = {m}")
print(f" 最大確率密度の半径: {r_max_prob:.3f} a₀")
print(f" エネルギー準位: E_{n} = -13.6/{n}² = {-13.6/n**2:.3f} eV")
print("\n" + "=" * 60)
print("凡例:")
print(" 赤色: 波動関数が正")
print(" 青色: 波動関数が負")
print(" 透明度: 外側ほど薄く表示")
print("=" * 60)

元ツイ主が理系なのか文系なのかわからないのですが、、数学的・物理的に正確な図を求める場合には、画像AIに頼るのではなく、いったんプログラムコードで正確に描画するほうが正しい図が得られるよ、という老婆心であります。
