2019年11月9日 星期六

Matplotlib 繪圖技巧:繪製選擇題用的函數圖形選項

作者:王一哲
日期:2019/11/9

前言


有時候選擇題會以五張不同的函數圖形作為選項,之前我是用 LibreOffice Draw 的繪圖工具畫好圖形之後再加上選項標籤,最後將五張函數圖形以及選項標籤匯出成一張圖片。下圖是我用 LibreOffice Draw 繪製的選項,題目是要選出自由落下的小球觸地時發生彈性碰撞的v-t圖,但是選項中的曲線畢竟是手動繪製的,只能算是示意圖。如果對於圖形的精準度要求不高,這樣的作法應該可以符合需求,但如果需要畫出精準的函數圖形,LibreOffice Draw 就不太適合了。


使用 LibreOffice Draw 繪製的函數圖形選項



於是我試著改用 NumPy 計算函數值,再用 Matplotlib 中的 subplots 將圖形及標籤一口氣畫好,以下是實際出題的例子。



使用 NumPy + Matplotlib 繪製都卜勒效應題目的選項


試題


假設聲源發出的聲波頻率為 1000 Hz,空氣中的聲速為 340 m/s,下表中是兩種不同狀況下觀察者觀測到的聲波頻率,單位為 Hz。狀況 I:聲源移動速度為 v 接近靜止的觀察者;狀況 II:觀察者移動速度為 v 接近靜止的聲源。下列的 5 張圖形中,請問何者最符合表格中數據?

v (m/s) 0 5 10 15 20 25 30 35 40
狀況 I 測到的聲波頻率 (Hz) 1000.0 1014.9 1030.3 1046.2 1062.5 1079.4 1096.8 1114.8 1133.3
狀況 II 測到的聲波頻率 (Hz) 1000.0 1014.7 1029.4 1044.1 1058.8 1073.5 1088.2 1102.9 1117.6








圖形對應的函數


假設波源移動時觀測到的聲波頻率為$f_s$,觀察者移動時觀測到的聲波頻率為$f_o$,空氣中的聲速為$v_0$,波源發出的聲波頻率為$f_0$,五張圖形對應的函數如下:
$$f_{s,1} = \frac{v_0}{v_0 - v} f_0 ~~~~~~~~~~ f_{o,1} = \frac{v_0 + v}{v_0} f_0$$
$$f_{s,2} = \frac{v_0 + v}{v_0} f_0 ~~~~~~~~~~ f_{o,2} = \frac{v_0}{v_0 - v} f_0$$
$$f_{s,3} = \frac{v_0 + v}{v_0 - v} f_0 ~~~~~~~~~~ f_{o,3} = \frac{v_0 + v}{v_0} f_0$$
$$f_{s,4} = \frac{v_0}{v_0 + v} f_0 ~~~~~~~~~~ f_{o,4} = \frac{v_0 - v}{v_0} f_0$$
$$f_{s,5} = \left( \frac{v_0}{v_0 - v} \right)^2 f_0 ~~~~~~~~~~ f_{o,5} = \left( \frac{v_0 + v}{v_0} \right)^2 f_0$$




程式碼


import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

plt.rc('font', **{'family' : 'sans-serif'})
plt.rc('legend', fontsize=16)

f0, v0 = 1000, 340   # 原來的聲波頻率, 空氣中的聲速
# 產生陣列 v, fs, fo 共 5 組
vmin, vmax, num = 0, 40, 100
v = np.linspace(vmin, vmax, num)
fs1 = v0 / (v0 - v) * f0   # 正確選項
fo1 = (v0 + v) / v0 * f0
fs2 = (v0 + v) / v0 * f0   # 錯誤選項
fo2 = v0 / (v0 - v) * f0
fs3 = (v0 + v) / (v0 - v)* f0   # 錯誤選項
fo3 = (v0 + v) / v0 * f0
fs4 = v0 / (v0 + v) * f0   # 錯誤選項
fo4 = (v0 - v) / v0 * f0
fs5 = (v0 / (v0 - v))**2 * f0   # 錯誤選項
fo5 = ((v0 + v) / v0)**2 * f0

# 用 plt.subplots
fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(2, 3, figsize=(12, 8), dpi=72)
fig.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9, wspace=0.4, hspace=0.3)

# 繪圖並設定線條顏色、寬度、圖例
line1_1, = ax1.plot(v, fs1, color='black', linestyle='-', linewidth=3, label='I')
line1_2, = ax1.plot(v, fo1, color='black', linestyle='--', linewidth=3, label='II')
ax1.legend(handles = [line1_1, line1_2], loc='upper left')
ax1.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax1.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax1.tick_params(axis='both', labelsize=14)
ax1.set_title('(A)', loc='left', fontsize=20)

line2_1, = ax2.plot(v, fs2, color='black', linestyle='-', linewidth=3, label='I')
line2_2, = ax2.plot(v, fo2, color='black', linestyle='--', linewidth=3, label='II')
ax2.legend(handles = [line2_1, line2_2], loc='upper left')
ax2.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax2.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax2.tick_params(axis='both', labelsize=14)
ax2.set_title('(B)', loc='left', fontsize=20)

line3_1, = ax3.plot(v, fs3, color='black', linestyle='-', linewidth=3, label='I')
line3_2, = ax3.plot(v, fo3, color='black', linestyle='--', linewidth=3, label='II')
ax3.legend(handles = [line3_1, line3_2], loc='upper left')
ax3.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax3.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax3.tick_params(axis='both', labelsize=14)
ax3.set_title('(C)', loc='left', fontsize=20)

line4_1, = ax4.plot(v, fs4, color='black', linestyle='-', linewidth=3, label='I')
line4_2, = ax4.plot(v, fo4, color='black', linestyle='--', linewidth=3, label='II')
ax4.legend(handles = [line4_1, line4_2], loc='upper right')
ax4.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax4.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax4.tick_params(axis='both', labelsize=14)
ax4.set_title('(D)', loc='left', fontsize=20)

line5_1, = ax5.plot(v, fs5, color='black', linestyle='-', linewidth=3, label='I')
line5_2, = ax5.plot(v, fo5, color='black', linestyle='--', linewidth=3, label='II')
ax5.legend(handles = [line5_1, line5_2], loc='upper left')
ax5.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax5.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax5.tick_params(axis='both', labelsize=14)
ax5.set_title('(E)', loc='left', fontsize=20)

ax6.axis('off')   # 隱藏右下角小圖

fig.savefig('DopplerEffectPlot.svg')
fig.savefig('DopplerEffectPlot.png')
fig.show()



整個程式可以分為兩大部分:

  1. 第 8 - 21 行:定義圖形對應的函數並產生資料。
  2. 第 24 - 72 行:繪製圖形並儲存成圖檔,雖然看起來行數很多,但其實只是同樣的事做了5次,由於我是用 numpy.subplots 指令產生 2 列、3 欄共 6 張小圖,但是只需要 5 張圖作為選項,因此最後用 ax6.axis('off') 隱藏右下角的小圖。



結語


雖然這樣的作法看起來很麻煩,但是以後如果要出其它的題目,只要修改函數以及 x 軸範圍再執行一次即可,畫出來的函數圖形絕對正確,而且圖形的風格能夠保持一致,以後出題會更方便。




參考資料


StackOverFlow https://stackoverflow.com/questions/10035446/how-can-i-make-a-blank-subplot-in-matplotlib





HackMD 版本連結:https://hackmd.io/@yizhewang/ryLJ7RZcr

沒有留言:

張貼留言