兩個聽起來像一個的問題
上一篇指南主張你不該自己手寫數值核心——你該站在調校過的函式庫如BLAS 與 LAPACK之上,而不是親手寫內層迴圈。但信任一個函式庫只是地板。當你把那些零件組裝成對某個真實事物的模擬——一根散熱的桿、一根彎曲的樑、一條管裡的流體——的那一刻,一個更深的問題出現了:我螢幕上的答案「正確」嗎?這篇指南要把那一個模糊的問題拆成兩個鋒利的問題,因為它們以不同的方式失敗、你以不同的方式除錯,而把它們搞混,正是好看的模擬悄悄說謊的方式。
這兩個問題有著整個領域共用的名字。驗證(verification)問的是:「我有沒有把方程式解對?」也就是,我的程式是否忠實地產出了我聲稱要解的那個數學模型的解——沒有臭蟲,沒有偷偷收斂到錯誤對象的格式。確認(validation)問的是:「我解的是不是對的方程式?」也就是,那個數學模型是否真的描述了我在乎的物理現實。這句值得一字一句背下來的口號是:驗證是把方程式解對;確認是解對的方程式。
一根漏水的管:每種誤差從哪裡進來
把從現實到你螢幕上一個數字的旅程,想像成一根有三個接頭的管,誤差可以從每一個接頭漏進來。第一,現實被擠進一個數學模型——你決定把空氣當作連續流體、忽略摩擦、假設那根桿完全均勻。現實與那個理想化模型之間的落差,就是建模誤差,而那正是確認要獵捕的對象。第二,那個模型——通常是一條你無法手算的微分方程式——被換成一個離散的近似:一張網格、一個有限差分樣板、一張有限元素網。那裡的落差是離散化誤差,它會隨你加密網格而縮小。第三,那個離散問題由你的程式以浮點運算求解,引入了捨入誤差,運氣不好時,還有純粹的臭蟲。
現在分工就乾淨了。驗證住在第二與第三個接頭:它問的是,當網格加密、捨入受到控制時,你的程式是否真的收斂到「離散再連續」那個模型的真解——它管的是離散化誤差與臭蟲。確認住在第一個接頭:它問的是,那個模型即使被完美求解,是否吻合現實——它管的是建模誤差。深層的陷阱是,這兩種誤差可能會「互相抵消」。一個有臭蟲的程式在粗網格上,可能碰巧吻合了某個實驗,因為兩個錯誤恰好加成了一個對的結果。那種一致毫無價值,而唯有把驗證和確認分開,才能揭穿它。
你實際上怎麼驗證:讓答案變成已知
驗證有一個漂亮而具體的方法,因為它是純數學:要檢查你的程式有沒有把方程式解對,就拿它的輸出去對照一個你「早已精確知道」的解。這種已知答案最乾淨的來源,是製造解法,也就是緊接著下一篇指南的主題。這個想法把平常的流程倒過來。你不是從一條方程式出發、苦苦尋找它的解,而是憑空「挑」一個平滑的解——比方說 u(x) = sin(x)——把它代進你的微分算子,讓代數告訴你:什麼樣的源項與邊界值,會讓那個正好成為你的精確解。現在你手上有一個答案你知道到最後一位的問題,你可以把它餵給你的程式。
但在單一網格解析度下吻合一個已知答案還不夠——捨入與運氣可以偽造它。驗證的黃金標準是一場收斂研究:在間距 h、h/2、h/4 的一連串網格上求解那個製造出來的問題,看誤差如何縮小。你的格式帶著一個承諾的準確度階數——一階方法的誤差應是 O(h),二階方法是 O(h^2)。如果當你把 h 砍半時,一階方法的誤差大約掉成 1/2、二階方法大約掉成 1/4,那麼「觀察到的」階數就吻合「理論的」階數,這便是僅次於證明、最強的證據,說明你的程式沒有臭蟲。如果誤差就是不肯按承諾的速率下降,你就有臭蟲——即使每個單獨的答案看起來都很合理。
verify by manufactured solution + convergence study
1. pick exact u*(x) e.g. u*(x) = sin(x)
2. f(x) := L[u*](x) plug u* into your operator L, read off the source
3. for h in {h, h/2, h/4}:
u_h := solve L[u]=f on grid of spacing h
e_h := max | u_h - u* | (the error you CAN compute)
4. observed order p ~ log2( e_h / e_{h/2} )
5. compare p to the scheme's promised order
matches -> strong evidence the code is correct
too low -> there is a bug, no matter how 'reasonable' u_h looked你怎麼確認,以及為何它永遠無法被「證明」
確認比較謙卑,也比較難,因為沒有精確答案可以對照——現實不會遞給你一條公式。取而代之,你拿你已經驗證過的模擬去對照量測:一個風洞壓力、一個熱電偶讀數、一個臨床觀察到的劑量。一個密切相關的做法是基準問題——一個標準測試案例(蓋驅動空腔流、繞過圓柱的流動),它有一個來自仔細實驗、或來自另一個經充分驗證的程式的可信參考答案,於是一個新程式就能拿去對照社群累積下來的真理。
這裡有個關於確認的、誠實而略微令人不安的真相:它永遠無法被「完成」,只能被支持或反駁。與某一個實驗一致,只「在那個régime(情境範圍)」驗證了你的模型。一個在低速下確認過的氣流模型,在接近音速時可能完全錯誤;一個套合昨天數據的模型,明天可能失效。這不是一個待修的瑕疵——它就是建模的本性。確認在一個受測的範圍內買給你有理由的信心,在範圍之外卻從不是保證。而某些問題永遠在能力範圍之外:三維納維-斯托克斯方程式的平滑解是否總是保持平滑,是數學裡一個著名的未解問題,所以再多的湍流模擬,也無法拿去對照一個我們自己都還不擁有的正則性。
把它變成習慣:保持綠燈的測試
驗證與確認不是你在發表論文前做一次、然後就忘掉的儀式。程式會變;有人優化了一個迴圈、換掉一個求解器、重構了一個邊界條件——於是悄悄弄壞了上週還好好的收斂。防線是回歸測試:把你的製造解收斂研究與基準比較,變成在每一次改動時都會跑的自動化測試。如果一個「加速」悄悄把你的方法從二階掉成一階,一個從綠變紅的測試會在當天下午就抓到它,而不是在六個月後一位審稿人的電子郵件裡。
在你信任任何一個綠勾之前,有兩個實務上的提醒。第一,一個驗證測試必須夠敏感:要斷言「觀察到的準確度階數」,而不只是「誤差很小」,因為一個壞掉的二階格式仍可能產出很小、卻就是不再改善的誤差。第二,在你自己的腦中與測試套件裡,要把這兩件活動清楚標記——一個通過的驗證測試,對你的物理是否正確一無所言;一個成功的確認,對你的程式在未受測情境裡是否有臭蟲也一無所言。它們是分開的兩本帳,而本級接下來的三篇指南會依序把每一個磨利:收斂研究與製造解,然後是可重現性,再來是誠實的誤差棒。