# VPython 進階教材:小球鉛直簡諧運動與橫波
> 作者:王一哲
> 第1版:2020/10/19
> 第2版:2020/12/5 補充不使用 NumPy 的寫法
<br />
## 前言
當我需要繪製橫波的動畫時,通常是使用 GeoGebra 搭配以下的數學式
$$
f(x) = R \sin(kx - \omega t) = R \sin \left(\frac{2 \pi}{\lambda} \cdot x - \frac{2 \pi}{T} \cdot t \right)
$$
<img style="display: block; margin-left: auto; margin-right: auto" height="100%" width="100%" src="https://lh4.googleusercontent.com/f-3MZb7NX3Fhdz2rJYLwSbItnIh022PsZQRTHnQiw5ZqJrxzuP-RNEzWXsSJSR9Sowi3fmIpXbXv4Q2M_xvD67TGg5lmeXBZxt5kxQtlb01mlEihJySO55ReTpvXegXkQITKm6S5">
<div style="text-align:center">使用 GeoGebra 繪製的橫波動畫</div>
<br />
橫波上的每個質點在鉛直方向做簡諧運動,如果想要用 VPython 做出一樣的效果,理論上只要讓一排的小球每隔固定的時間差落到正下方的彈簧上,小球與彈簧結合後開始做鉛直簡諧運動即可。為了盡量避免使用 for 迴圈,我試著使用 numpy 陣列 (ndarray) 取代大部分的串列 (list),以下是我測試成功的動畫及程式碼。
<div style="text-align:center"><iframe src="https://www.glowscript.org/#/user/yizhe/folder/Public/program/ParticleWave" width="600" height="600"></iframe></div>
<div style="text-align:center">使用 VPython 畫出小球做鉛直簡諧運動模擬橫波的動畫</div>
<br />
## VPython 程式碼
```python=
"""
VPython教學: 使用多個質點的鉛直簡諧運動組成橫波
日期: 2020/10/19
作者: 王一哲
"""
from vpython import *
import numpy as np
"""
1. 參數設定, 設定變數及初始值
"""
m, r, h = 0.1, 0.3, 4 # 小球質量, 半徑, 起始高度
g = 9.8 # 重力加速度
k, L0 = 1.0, 8 # 彈簧彈性常數, 原長
T = 2*pi*sqrt(m/k) # 簡諧運動週期
t, dt = 0, 0.0005 # 時間, 時間間隔
length, num = 8, 41 # 波長, 小球個數
"""
2. 畫面設定
"""
# 動畫視窗
scene = canvas(title="Particles and Wave", width=600, height=600, x=0, y=0,
background=color.black, range=1.7*length, center=vec(1.5*length, 0, 0))
# 地板
floor = box(pos=vec(1.5*length, -L0, 0), size=vec(3*length, r, length), color=color.blue)
xs = np.linspace(0, 3*length, num) # x 坐標
ys = np.ones(num)*h # y 坐標
vs = np.zeros(num) # y 方向速度
ay = np.ones(num)*(-g) # y 方向加速度
delays = np.linspace(0, 3*T, num) # 時間延遲
states = np.full(num, False) # 小球與彈簧是否接觸
# 小球
balls = [sphere(pos=vec(xs[i], ys[i], 0), radius=r, color=color.red) for i in range(num)]
# 彈簧
springs = [helix(pos=vec(xs[i], -L0, 0), radius=0.6*r, thickness=0.4*r, axis=vec(0, L0, 0), color=color.yellow) for i in range(num)]
"""
3. 物體運動部分
"""
while True:
rate(1000)
states[ys <= 0] = True
ay[np.nonzero(states == True)] = -g - k*ys[np.nonzero(states == True)]/m
vs[delays <= t] += ay[delays <= t]*dt
ys += vs*dt
for i in range(num):
balls[i].pos = vec(balls[i].pos.x, ys[i], 0)
if states[i]:
springs[i].axis = vec(0, ys[i]+L0, 0)
t += dt
```
<br />
1. 第 28 ~ 33 行:這裡利用了幾個 numpy 陣列儲存資料,xs 是小球與彈簧的 x 坐標,ys 是小球的起始高度,vs 是小球的初速度,ay 是小球的 y 方向加速度,delays 是小球開始落下的時間,states 是小球與彈簧是否接觸的狀態。使用到的 numpy 陣列函數有
```python
np.linspace(起始值, 結束值, 等分數量) # 將起始值到結束值之間按照數量等分並回傳為陣列
np.zeros(長度) # 依照指定的長度產生所有元素皆為0的陣列
np.ones(長度) # 依照指定的長度產生所有元素皆為1的陣列
np.full(長度, 值) # 依照指定的長度產生所有元素皆為輸入值的陣列
```
2. 第 36 ~ 39 行:利用單行的 for 迴圈將小球 balls 及彈簧 springs 物件儲存到串列中。
3. 第 46 行:為了簡化算式,彈簧頂端起始高度為 y = 0,當 ys 由大於 0 變為小於等於 0 時,代表小球與彈簧接觸,state 指定為 True。
4. 第 47 行:當小球與彈簧接觸後, 彈簧形變量 = ys。當 ys < 0 時,彈簧回復力方向向上,因此 k\*ys 需要加上負號。再使用 np.nonzero(states == True) 列出小球與彈簧已接觸的元素索引值,將加速度由 -g 更新為 -g - k\*dy/m。np.nonzero(states == True) 也可以簡化成 np.nonzero(states)。
5. 第 48 行:當時間 t 大於等於該小球的時間延遲值時,更新小球的速度。
6. 第 50 ~ 53 行:使用 for 迴圈更新小球的高度, 若小球與彈簧已接觸則更新彈簧的軸向量為 (0, ys[i]+L0, 0)。
<br />
## 不使用 NumPy 的寫法
```python=
"""
VPython教學: 使用多個質點的鉛直簡諧運動組成橫波, 不使用 NumPy
日期: 2020/12/5
作者: 王一哲
"""
from vpython import *
"""
1. 參數設定, 設定變數及初始值
"""
m, r, h = 0.1, 0.3, 4 # 小球質量, 半徑, 起始高度
g = 9.8 # 重力加速度
k, L0 = 1.0, 8 # 彈簧彈性常數, 原長
T = 2*pi*sqrt(m/k) # 簡諧運動週期
t, dt = 0, 0.0005 # 時間, 時間間隔
length, num = 8, 41 # 波長, 小球個數
"""
2. 畫面設定
"""
# 動畫視窗
scene = canvas(title="Particles and Wave", width=600, height=600, x=0, y=0,
background=color.black, range=1.7*length, center=vec(1.5*length, 0, 0))
# 地板
floor = box(pos=vec(1.5*length, -L0, 0), size=vec(3*length, r, length), color=color.blue)
xs = [i*3*length/(num-1) for i in range(num)]
ys = [h]*num
vs = [0]*num
ay = [-g]*num
delays = [i*3*T/(num-1) for i in range(num)]
states = [False]*num
# 小球
balls = [sphere(pos=vec(xs[i], ys[i], 0), radius=r, color=color.red) for i in range(num)]
# 彈簧
springs = [helix(pos=vec(xs[i], -L0, 0), radius=0.6*r, thickness=0.4*r, axis=vec(0, L0, 0), color=color.yellow) for i in range(num)]
"""
3. 物體運動部分
"""
while True:
rate(1000)
for i in range(num):
if ys[i] <= 0:
states[i] = True
if states[i]:
ay[i] = -g - k*ys[i]/m
if delays[i] <= t:
vs[i] += ay[i]*dt
ys[i] += vs[i]*dt
balls[i].pos = vec(balls[i].pos.x, ys[i], 0)
if states[i]:
springs[i].axis = vec(0, ys[i]+L0, 0)
t += dt
```
<br />
主要修改以下兩處,修改後的程式碼可以在 GlowScript 網站上執行,這是 [GlowScript 網站動畫連結](https://www.glowscript.org/#/user/yizhe/folder/Public/program/ParticleWave)。
1. 第 27、31 行:將原來的 numpy 陣列全部改成 list,並且使用單行的 for 迴圈縮短程式碼。計算 xs 及 delays 時需要除以 num-1 而不是 num,因為 41 個小球之間只有 40 個間隔。
2. 第 45 ~ 55 行:使用 for 迴圈逐一更新小球及彈簧的狀態。
<br />
## 結語
由於 numpy 陣列有許多很神奇的函數及使用方法,如果能夠善加利用可以少寫很多個 for 迴圈,有效提升程式的運算速度。但是 VPython 的物件只能儲存到串列中,因此在更新整串的 VPython 物件時仍然需要使用 for 迴圈,這是無法避免的。
<br />
---
###### tags:`VPython`