10.3 專案三:科學計算工作流#
本節目標#
- 掌握四段式接力:SymPy 推導公式 →
lambdify轉函數 → NumPy 批量計算 → Matplotlib 視覺化 - 以「含空氣阻力的拋體運動」為完整範例走完全流程
- 學會 Jupyter Notebook 的組織技巧
- 建立可重現的專案資料夾(
requirements.txt、隨機種子) - 實驗:把之前章節的實驗打包成 notebook
四段接力總覽#
| 階段 | 工具 | 產出 |
|---|---|---|
| 推導 | SymPy(符號) | 公式(運算式物件) |
| 轉換 | lambdify | 快速 Python/NumPy 函數 |
| 計算 | NumPy | 陣列、大量數值結果 |
| 呈現 | Matplotlib | 圖形、結論 |
為什麼要接力? SymPy 擅長「推出公式」但算大量數值很慢; NumPy 擅長「批量計算」但不會微分積分。 lambdify 是兩個世界的橋梁(詳見 4.1)。
完整範例:含空氣阻力的拋體運動#
第一段:SymPy 推導#
一個質量 \(m\) 的拋體,速度 \(v\) ,受線性空氣阻力 \(-bv\) (一維垂直分量先做):
\[ m\frac{dv}{dt} = -mg - bv \]
用 SymPy 解出 \(v(t)\) 與高度 \(y(t)\) :
import sympy as sp
t = sp.Symbol('t', nonnegative=True)
v = sp.Function('v')
m, b, g = sp.symbols('m b g', positive=True)
ode = sp.Eq(m*v(t).diff(t), -m*g - b*v(t))
sol_v = sp.dsolve(ode, v(t), ics={v(0): sp.Symbol('v0')})
print(sp.simplify(sol_v.rhs))
# v(t) = (b*v0 + g*m*exp(-b*t/m) - g*m)/b ... 化簡後
# 積分得到高度
y_expr = sp.integrate(sol_v.rhs, t)
y_expr = sp.simplify(y_expr.subs(t, t) )
print(y_expr)
再推「落地時間」與「射程」的符號式(垂直分量):
# 最大高度:v(t) = 0 的時刻
t_top = sp.solve(sp.Eq(sol_v.rhs, 0), t)[0]
h_max = sp.simplify(sp.integrate(sol_v.rhs, (t, 0, t_top)))
print(h_max) # 符號表示的最大高度
第二段:lambdify 轉函數#
v0_, b_ = 30.0, 0.5 # 初速 30 m/s,阻力係數 0.5
m_, g_ = 1.0, 9.8
params = {v0: v0_, b: b_, m: m_, g: g_}
v_func = sp.lambdify(t, sol_v.rhs.subs(params), 'numpy')
h_func = sp.lambdify(t, h_max.subs(params), 'numpy')
第三段:NumPy 批量計算#
import numpy as np
tt = np.linspace(0, 6, 300)
vv = v_func(tt)
print(f"最大高度(符號公式): {float(h_max.subs(params)):.2f} m")
如果我們要比較 10 種阻力係數,lambdify 出的函數可以直接批量餵陣列—— 這正是 NumPy 的強項。
第四段:Matplotlib 視覺化#
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 4))
for b_val in [0, 0.2, 0.5, 1.0]:
p = {v0: v0_, b: b_val, m: m_, g: g_}
f = sp.lambdify(t, sp.integrate(sol_v.rhs, t).subs(p), 'numpy')
plt.plot(tt, f(tt), label=f'b = {b_val}')
plt.xlabel('t (s)'); plt.ylabel('y (m)')
plt.title('不同空氣阻力下的高度曲線')
plt.legend(); plt.grid(True)
plt.show()
解讀: \(b = 0\) 是理想拋物線;阻力越大,最大高度越低、 下落越慢——符號推導一次、數值比較多組,這就是接力的威力。
Jupyter Notebook 組織技巧#
- Markdown 混排:每段程式碼前後用 markdown 儲存格說明 「要做什麼 → 程式 → 觀察到什麼」,讓 notebook 自己會說話。
%matplotlib inline:讓圖形直接嵌在輸出區(新版 Jupyter 已內建,但寫著無害)。- 一個 notebook 一個問題:不要把三個專案塞在同一份。
- Restart & Run All:交付前從頭執行一遍,確認沒有依賴「亂序執行」的隱藏狀態。
- 用
display(sp.Eq(...)):把符號式以 LaTeX 排版呈現,比print易讀得多。
建議的 notebook 骨架:
# 標題:含空氣阻力的拋體運動
## 1. 問題與假設 (markdown)
## 2. 符號推導 (code: sympy)
## 3. 轉為數值函數 (code: lambdify)
## 4. 批量計算與比較 (code: numpy)
## 5. 視覺化 (code: matplotlib)
## 6. 結論 (markdown)
資料夾與可重現性#
一個可重現的專案長這樣:
projectile-lab/
├── notebook.ipynb # 主工作區
├── requirements.txt # 依賴清單
├── data/ # 原始資料(若有)
├── output/ # 產出的圖與表
└── README.md # 怎麼執行、結論摘要
requirements.txt(固定版本,別人才裝得出同樣環境):
sympy==1.13
numpy==1.26
matplotlib==3.8
jupyter==1.0
隨機種子:只要程式裡有隨機性(蒙地卡羅、隨機取樣),開頭就固定種子:
import numpy as np
rng = np.random.default_rng(seed=42) # 固定種子,結果可重現
檢查清單:換一台機器 clone 下來,
pip install -r requirements.txt後 Restart & Run All 能跑出同樣結果,才算「可重現」。
實驗:把之前任一章的實驗包成 notebook#
以 10.1 的「旋轉體體積」為例,打包步驟:
- 開新 notebook,第一格放說明 markdown(題目、假設、預期答案)。
- 第二格放 imports 與參數(集中管理,方便改):
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
x = sp.Symbol('x', nonnegative=True)
f = sp.sqrt(x) # 想換題目只改這裡
a, b_ = 0, 4
- 推導格:
V = sp.pi * sp.integrate(f**2, (x, a, b_))並display(V)。 - 檢查格:
V.evalf()與 NumPy 取樣對照。 - 圖格:
sp.plot(f, (x, a, b_))加上旋轉體的示意(圓盤半徑 \(f(x)\) )。 - 結論格:用 markdown 寫下「答案、檢查方式、學到什麼」。
驗收標準:送給同學,對方能不看你本人就重跑出全部結果與結論。
小結與練習#
本節建立了 SymPy → lambdify → NumPy → Matplotlib 的四段接力工作流, 以空氣阻力拋體為完整範例演練;並整理了 Jupyter 組織技巧與 可重現性要求(requirements.txt、隨機種子、Restart & Run All)。 從下一節開始,我們要走向更廣的科學計算生態。
練習
- 把 10.1 的「極值應用題(菜園問題)」打包成 notebook, 要求包含符號推導、數值對照、畫圖三段。
- 在拋體範例中改用二次阻力 \(-bv^2\) (只對向下運動合理即可), 推導並數值比較 \(b = 0.2, 0.5, 1.0\) 的最大高度。
- 為你的專案資料夾寫
README.md與requirements.txt, 交給同學測試能否在另一台機器重現。 - 用固定種子的蒙地卡羅法估計 \(\pi\) (隨機投點法), notebook 裡畫出「投點數 vs 誤差」的收斂圖,並說明種子如何保證可重現。