2016年3月14日 星期一

濾波器發散 (Filter Divergence)


    系集卡爾曼濾波器(Ensemble-based Kalman Filter, EnKF)是利用系集預報後,各系集成員間的差異來估計背景誤差結構,而EnKF的表現取決於系集能否完整的表達真實大氣的特性,這牽扯到模式的動力是否完整正確、系集的樣本數是否足夠等因素。實作上因受限於計算資源,系集樣本數不可能太大,故EnKF經常面臨樣本數不足的問題。當系集樣本數不足(註一)時,除會引入取樣誤差外,更可能造成濾波器失敗(Filter Failure)。
  1. 較傳統的定義是指系集在動力模式中發展時,因樣本數不足,使得系集的差異性(離散度)不足,低估真實大氣的不確定性。這會讓EnKF認為此時的預報不確定性很低,模式已可掌握真實大氣的發展,也就不需過多的觀測資訊修正背景場。少了觀測資訊的修正,系集的離散度會隨著時間繼續下降,使得觀測資訊更難藉由同化過程修正背景場,最終致使系集收斂至模式動力特徵,完全背離真實大氣的狀態,導致濾波器發散(Filter Divergence)。 
  1. 另一種濾波器失敗稱作Catastrophic Filter Divergence(CFD),同樣是因系集樣本數不足造成。這種形態的濾波器失敗是模式在積分過程中,會於短時間內出現無限大的值(machine infinite),讓積分結果爆掉(blow-up),屬於數值不穩定造成的濾波器失敗。 
    Catastrophic Filter Divergence的觸發條件是:
  1. 觀測誤差很小,確保觀測資料一定會被使用。傳統的濾波器發散是因為觀測誤差相對背景誤差過大,使得觀測資訊無法被使用。而對CFD來說,反倒是需要很小的觀測誤差,除了確保觀測資訊一定會被使用外,因為觀測誤差很小,使得背景場會大幅度地朝觀測值靠攏。
  1. 需在特定的同化間隔(註二)才會觸發,當同化間隔過高或過低都不會觸發。若同化間隔過高,則分析場會非常接近觀測場;若同化間隔過高,則分析場會隨著積分過程而具有模式動力的特徵。但若是同化間隔不高也不低時,當獲得一個接近觀測值的分析場後,隨模式積分一小段時間,還沒獲得模式動力平衡時,又馬上被拉回觀測值。如此周而復始的在觀測值與模式動力平衡間來回調整將會誘發數值不穩定,進而讓結果在積分過程中爆掉(blow-up)。


註一:怎樣的系集樣本數才足夠?理論上,一個系統自身的不確定性可以用Lyaponov exponent來表示,假設某系統的Lyaponov vector是50,表示只要有50組獨立且有效的系集成員就可以表達此系統的不確定性。但實作上Lyaponov exponent 並不容易獲得,也沒有意義(因為NWP模式的複雜度太高,即便知道Lyaponov exponent 的大小,也沒有能力負擔相對應的系集數)。


註二:同化間隔是否適當取決於模式解析度與天氣系統的尺度。如氣候模式與雲解析度模式適用的同化間隔就不一樣。

<參考資料>
Harlim, J., and A. J. Majda, 2010: Catastrophic filter divergence in filtering nonlinear dissipative systems. Communications in Mathematical Sciences, 8, 27–43, doi:10.4310/cms.2010.v8.n1.a3.
Gottwald, G. A., and A. J. Majda, 2013: A mechanism for catastrophic filter divergence in data assimilation for sparse observation networks. Nonlinear Processes in Geophysics, 20, 705–712, doi:10.5194/npg-20-705-2013.


    濾波器失敗可以分為兩種不同型態: 
    這兩種濾波器失敗的型態雖都稱作濾波器發散(filter divergence),但對NWP而言,意義卻不太相同。第一種(傳統)濾波器發散發生時,模式仍能提供預報結果,只是預報結果完全背離真實大氣狀態,不具預報能力。若沒有仔細評估預報結果,可能會錯信預報結果。第二種濾波器發散是因為積分過程中引發的數值不穩定而造成。由於傳統的濾波器發散的發生有跡可循,可透過各種統計方法估計離散度或是透過背景誤差矩陣的擴張(Inflation)來避免。相對而言,第二種型態的濾波器發散就較難掌握,因此將就其觸發條件與發生原因進行討論。

    可以想像模式在積分時,是沿著模式動力的吸引子移動(達到模式動力的平衡),但因模式具有模式誤差,使得模式動力的吸引子並不相等於真實大氣狀態的吸引子,所以需要藉由資料同化將將預報從模式動力的吸引子拉向觀測值,以逼近真實大氣狀態的吸引子。(關於吸引子的概念,可以從Lorenz的蝴蝶圖想像,該圖具有兩個明顯的吸引子,可以類比左側的中心是模式動力吸引子,右側是真實大氣狀態的吸引子) 。因此若觀測資料無法被同化,將造成預報最終會收斂至模式動力的吸引子,完全遠離真實大氣狀態的吸引子,也就是第一種濾波器發散。若是觀測誤差很小,同化時,原本受模式動力影響的背景場會馬上移動到觀測附近,開始積分後又會從觀測附近朝模式動力的吸引子移動。如果同化間隔很小,代表預報結果剛從觀測值往模式動力吸引子移動沒多久,又馬上被拉回觀測值,因此能確保預報結果很貼近觀測值;若同化間隔很長,那就有足夠的積分時間將預報結果從觀測值拉回到模式吸引子。但當同化間隔介於兩者之間時,會造成預報已經在前往模式吸引子的路上,又突然被拉回觀測,然後再積分一小段後,又再被拉回觀測,使得預報不斷在模式吸引子與觀測間快速來回調整。這種快速且劇烈的調整使得積分過程變得相當不穩定(數值上),可能在某個同化完成後的積分過程就爆掉了。而且CFD並不是因為不穩定度累積超過某個門檻後觸發的產物,其發生機率是屬於Poisson Distribution,也就是隨機過程,並不能夠掌握何時會出現,因此在執行EnKF同化時,需特別注意同化間隔對於數值穩定度的影響。

2016年1月25日 星期一

如何提升程式可讀性

程式碼應易於理解。


因此撰寫程式時,應將讀者理解所需的時間降到最低,意即程式碼需具有最佳可讀性。此處所謂的完全理解是指讀者有能力修改,debug,並知道程式與其他部分互動的方式。程式的可讀性與效能最佳化兩者間未必衝突,甚至具有良好可讀性的程式因易於被理解、測試,將更有利於最佳化。然而可讀性的困難之處在於必須時時考量某位虛擬讀者能否理解,故初期(未養成習慣之前)會耗費更多時間,一旦建立風格後,將可大幅縮短額外耗費的時間。


可讀性的改善可由下列兩種概念著手:1. 表層改善。使程式碼在視覺上具有良好的可讀性,使讀者不會抗拒閱讀程式碼。2. 簡化迴圈與邏輯。使程式碼在心理上具有良好的可讀性,過於複雜的迴圈與邏輯、大量的變數都會提高讀者的心理負擔,需要更縝密思考和記憶更多變數才能理解程式碼,不利閱讀。


<表層改善>
表層改善的方法主要有三種:1.挑選合適的程式(變數)名稱,可使讀者迅速了解程式的目的。2.良好的註解,可適時的幫助讀者了解程式的運作。3. 具備良好程式美學,簡潔整齊具美感的程式碼能提升讀者的耐心。


  1. 合適的程式(變數)名稱
一個好的命名除了不會被誤解外,還要能提供足夠資訊使讀者瞭解,因此命名時需讓程式(變數)名稱具有意義,使名稱可成為註解的一部分。在完整表達含意之餘,如能兼具高資訊空間比將會是個成功的命名。

選擇詞彙時,須選擇意義明確並避免使用空洞的詞彙。因此善用英文的同義字,找出更合適、詞意更鮮明的字彙是命名時需費心的地方。同時,避免使用過於通用的名稱(如tmp),這些變數不僅有可能在程式碼中重複出現造成讀者混淆外,也不能提供額外的訊息,因此並不是理想的命名。然而,若此變數的生命期僅有數行,且此變數不具其他責任,不會被傳遞至其他函數,亦不會被重設或重複使用,在不影響讀者理解的前提下,為了撰寫者的便利還是允許使用這類變數。

最佳的命名應能說明變數的目的以及(或是)包含實體數值的訊息。如迴圈循環子(interator)的命名一般都是使用i,j,k,故在程式碼中若出現這些變數,讀者會直覺聯想到是interator,此種命名便清楚說明變數目的(但若是將i,j,k用在其他用途,就可能是個失敗的命名方式)。有時不得不在迴圈系統中使用許多巢狀結構時,適當命名interator也可協助讀者了解目前正處於哪段迴圈中。例如有三層迴圈,分別是觀測資料的迴圈、模式水平格點與垂直格點的迴圈,若將interator命名成i_obs、j_x、k_z,將有助讀者理解。

有時變數所存取的資料會有不同單位(或是不同屬性),如在命名時就提供這些資訊將可減輕讀者負擔,亦能幫助撰寫者除錯。例如時間相關變數,若單純命名為time,則可能在A段程式碼中讀取的時間單位是秒(sec),但B段中讀入的時間單位是毫秒(ms),如此不僅會混淆讀者,甚至是個潛在的bug。如將單位加入變數名稱,如time_s、time_ms,便能方便辨識。

因此,良好的命名不僅可增加可讀性,亦可避免bug的發生。命名時需跳脫撰寫者的身分,發揮自身想像力思考名稱是否具足夠資訊協助讀者了解此變數/程式碼的目的,並審慎思考該名稱是否過於空泛、模糊、間接,使讀者不僅無法一眼看出目的外,甚至造成讀者誤解。
為使命名能提供足夠訊息,在合適範圍內增加名稱的字元數不是壞事。一般命名原則是較小的範圍內可考慮使用較短的變數,若變數跨越的範圍較長,則可透過較長的名稱賦予較多訊息以利讀者理解。因為若變數生命期只有數行,名稱卻有20個字元,其所提供的資訊空間比就較低,不是理想的命名方式。較長的名稱對撰寫者而言無疑是個負擔,但大多數的編輯器都內建「識別字補齊」的功能,如在vi中,便可使用ctrl-p 幫忙補齊字元,在註解中也同樣適用。但要注意,並不是長的變數名稱就一定好,有時消除變數中的某些詞彙若不影響名稱所含的資訊量,就建議移除(如fromNumberToString,若拿掉from並不影響意思,故寫成number2string或num2str即可)。在不同型態的變數使用不同的命名規則也有助於讀者理解,如在類別名稱使用駝峰式大小寫,變數名稱使用底線分隔也是種方法,總之只要確立命名原則並妥善遵守(或是提供註解說明)都能增加可讀性。

有些撰寫者在命名時會使用縮寫,如rmse,若這種縮寫對同領域/專案的使用者而言相當常見(如在資料同化中,使用B代表背景誤差協方差矩陣),那就可被接受(然而,在程式碼中若直接將背景誤差協方差矩陣命名為B也不洽當),否則,還是盡量避免。

為避免造成使用者誤解,有些基本的命名原可作為參考,若數值包含邊界,可加上max_或min_,便可清楚知道數值的特性;變數名稱最好避免使用否定敘述,以免增加閱讀負擔;如能在命名時便告知使用者大概的計算量,那也是相當有用的資訊,例如在進行龐大計算時,可使用compute而非get(computeMean)。


  1. 良好的註解
註解的目的是協助讀者能快速地了解撰寫者的想法,同時提供撰寫過程中的經驗。一般而言,程式碼的名稱應能使讀者對此程式具有基本概念,然後程式碼的開端可提供詳細、更進一步的文字說明(全局註解)。每個大段落間也可透過註解告知讀者此段程式碼的目的,避免讀者深陷於細節之中。在撰寫註解時,並不一定得留下正式完整的文句,只要能提供重要的訊息,簡單的幾句話也可以。

程式碼中的註解除幫助讀者瞭解各區塊的目的外,也提供平台供撰寫者與讀者對話,如果撰寫方式因某些原因而和一般習慣用法不同,即可透過註解說明;如果撰寫者針對某部分的效能已有一些測試結果,同樣可以註解方式說明,避免維護者或後續使用者重複無謂的測試;定義常數時,對各常數的使用環境以及採用特定數值的原因均可透過註解使讀者瞭解;撰寫過程中,有時為了時效而忽略排版或效能,這些不足之處可通過註解說明改善方向以供後續使用者(可能是數週後的自己)跟進。一個良好有用的程式碼應會不斷被使用,也會持續地被改善,故於撰寫過程中忠實記錄發現的缺憾或改善的構想能讓讀者瞭解程式的狀態及品質。許多程式設計師會使用下列標記紀錄不足的部分:
TODO:紀錄作者尚未處理的部分 (若是大寫表示須優先處理,小寫則表示問題較小)。
FIXME:已知的問題。
HACK:承認解決方式不夠優雅。
XXX:危險! 相當重要的問題,需要立即解決。

然而,並非每一步驟都需要註解。技術上來說,若註解沒有提供新資訊,或註解的部分只需簡單觀察程式碼即可輕易知道工作內容,那這個註解就是無用的註解,並不需要為了註解而註解。如何決定一個註解是有意義的?除了上述的情況外,程式設計師會試圖從第三者的角度來重讀程式碼,預期讀者可能會出現的問題。若在重新閱讀時,發現某些程式碼的內容需要額外仔細思考,或對某些變數的存在感到疑惑時,就非得要註解不可(如果是對變數的命名感到疑惑,此時需要的不是註解,而是重新命名)。

簡單的說,適時寫下心中有意義的想法,就算只有幾個字也好,都可有效幫助讀者理解程式碼。但一個好的註解應具有「高資訊空間比」。與命名的原則相同,能妥善使用包含大量資訊的詞彙將可使註解更為簡潔;撰寫註解時需極力避免模稜兩可的的代名詞(如修好它,讀者並不一定知道"它"表示哪一部分);使用非內建或陌生函數時,必須精確描述函數的行為,最好提供輸入值與輸出值的範例(以及變數型態)以增進讀者瞭解,同時亦可幫助除錯。


  1. 程式美學
良好的程式碼應能使讀者賞心悅目,因此程式碼編排方式對可讀性相當重要,基本上可藉由一致的排版、將相似(關)的程式碼調整成有相似外觀以及將程式碼分段使程式碼具有基本美學架構。

為了排版的一致,在整個程式或專案中,最好維持相同的排版原則,如巢狀結構中空白字元的數量、段落間空白幾行等。為使整體版面能維持整潔,多幾行程式碼無傷大雅。

將相似(關)的程式碼調整成有相似外觀,如此除能使讀者一眼便可了解此區塊的程式碼具相同結構外,由於對應的位置相似,若出現打字錯誤的bug,也能夠快速找出。然而,若要將大量的程式碼調整成「列對齊」以提供較佳的視覺邊界,對撰寫者而言也是不小的負擔。當維護時,原本只是修改一行程式碼,卻為了使整體版面具有相似外觀反而需改動更多行程式碼。雖然麻煩,但這樣做的好處應遠大於所花費的時間。萬一程式碼真的過於龐大不易調整,到時再放棄就好。

適當的分段能讓讀者清楚知道目前所在位置,同時也易於閱讀,若能於每段都放入合適的註解將可大幅縮短讀者的理解時間。在某些情況下,程式碼的順序並不影響正確性,然若能賦予有意義的順序對程式維護與內容理解將有正面助益,透過重要性遞減排序,或是單純依照字母排序都是不錯的方式。

程式美學的標準見仁見智,但在專案中,保持一致的風格會比「正確」的風格更為重要。



<簡化迴圈與邏輯>
如果程式碼中沒有任何的迴圈或邏輯判斷,讀起來會非常容易,過於複雜的迴圈與邏輯判斷將會增加讀者的負擔,因此如何簡化程式碼中的迴圈與邏輯敘述對可讀性的提升相當重要。設計迴圈與邏輯敘述時,應使敘述盡量自然,讓讀者在閱讀過程中不需停下來重新思考。例如一般的程式設計者在設計迴圈與邏輯的條件敘述時,習慣將會改變的變數(也就是欲比較的對象)放在左側,而固定的常數(也就是比較的基準)放在右側,如果違背這個原則,讀者在閱讀時將會因程式碼的「不自然」敘述而停下腳步,雖不至於無法理解,但會降低閱讀速度。在撰寫if/else敘述時,通常能自由交換各區塊的順序,但若能先處裡肯定條件而非否定條件、先處裡簡單的情況、先處裡較有趣或明顯的情況,將可避免條件敘述是以較難理解的順序呈現,也是增進可讀性的好技巧。


有些程式設計師喜歡將複雜的概念用一行程式碼或是獨特的程式碼來表示,前者對讀者而言需要花費許多心力才可能領悟撰寫者的巧思;至於後者,雖然寫法獨特的程式碼看起來很新穎,也許能縮減程式碼行數,但會對讀者以及後續使用者造成困擾,如果是個人練習的實作無可厚非,但若是需要分享的程式碼,還是以增進程式可讀性為優先,縮短他人理解所需的時間比減少程式碼還重要。


多層巢狀結構對讀者相當不友善,每層巢狀結構都會造成讀者額外的心理負擔,因為當讀者看到某層巢狀結構結尾時,很難回憶起這部分屬於哪層巢狀結構以及其所表示的意義。所以除非必較,撰寫時盡量避免使用過於複雜的巢狀結構。巢狀結構的增長往往出現在修改的過程,重複的增修經常會導致最後出現複雜的巢狀結構。因此修改程式碼時,需以全新的角度重新審視,以整體的角度考慮程式碼的修改方式。一般來說,非得使用巢狀結構時,最好能及早跳脫並清除巢狀結構,使程式碼更為簡潔,因為每多一層巢狀結構,讀者就必須記憶更多的環境資訊。盡量使用線性的程式流程,避免巢狀結構。


和100行程式碼相比,一次閱讀3000行程式碼對讀者造成的心理負擔更重,研究顯示人類一次只能思考三到四種東西,因此程式碼越長就越難理解。在撰寫程式碼時,最好將巨大的內容分解成更容易消化的大小。這個概念相當重要,可應用於任何層級的程式碼中。最低階的層級如某行需要計算的程式碼,假設該行程式碼的工作是先讀取某個變數,再進行條件邏輯判斷,若只用一行表示會增加讀者閱讀負擔,故可使用「解釋性變數」先行讀取變數,並以變數名稱告知讀者正在進行的事,再使用第二行程式碼進行邏輯判斷,雖然這會多出一個變數及一行程式碼,但對整體程式碼的負擔並不顯著,且能大幅提升可讀性;相同的概念,假設某個變數陣列中有五個變數,但計算過程中只需使用其中某變數,此時便可利用「摘要變數」來增加可讀性。摘要變數目的是用較短、較易了解的名稱,於小範圍程式碼中取代較長或複雜的變數名稱。利用這兩種技巧,可避免在程式碼中不斷重複相同的步驟(DRY, Do not Repeat Yourself),不僅能避免重複打字造成的錯誤外,也可大幅減少程式碼的負擔,維護時若欲修改名稱也只需更改一次。


更高的層級是思考該如何重新組織整個程式碼,找出程式碼中會重複執行的區塊,並試圖用副程式來處理。這樣的好處不只能縮短程式碼,將程式內容以副程式獨立出來也易於維護與測試,甚至副程式還能被其他程式呼叫,成為通用的副程式碼。在重新組織程式碼時,有兩個很重要的概念:1.積極尋找並抽離不相關的子問題2.一次處理一個問題


在程式撰寫的過程中,有個相當重要的基本原則就是將大問題分解成小問題,再將小問題的解答組合成原本大問題的解答。因此如何找尋可分離且值得分離的部分就是個值得程式設計師認真思索的問題。初學者的話,可先閱讀特定的函數或程式區塊,並自問這段程式的主要目的為何?接著再細看每行程式碼,自問每行程式的工作內容與這段程式碼的工作目的是否直接相關?還是這行程式是為了處理與目的無關的子問題?若用以解決子問題所花費的行數過多,就值得抽離並寫成獨立的函數或副程式。例如在計算兩點間距時,需處理球面的影響,座標轉換的過程相當複雜且一再重複,所花費的程式行數也相當龐大,故若將此部分以副程式表示,即可簡潔程式碼,並使撰寫者能專注於高階目的,不需一直耗費心思處理幾何公式,且這個副程式也可被重複使用,是個相當值得的投資。同時,將子問題抽離後,也較容易加入新功能、改善可靠度或進行效能測試。因此一個理想的專案,應會將許多常見的基本子問題抽離,並放置於特定資料夾供所有撰寫者使用。


但須注意,萬物皆過猶不及。若將程式碼分解過頭反而會造成讀者的負擔,讀者必須記憶各副程式輸入與輸出的訊息,也可能需要在執行的路徑上來回跳躍。因此撰寫者必須清楚了解加入新的副程式必須付出微小(但明確且無可避免)的可讀性成本,若付出的成本並無法獲得相對應的收穫,就不需要立刻將此部分寫成副程式,可等到其他部分也面臨同樣需求時再來考慮。


如前所述,人的大腦一次能思考的東西並不多,若在程式碼中同時進行許多工作,將對讀者造成嚴重負擔,對撰寫者而言也可能是個潛在bug。故應將程式碼重新組織為一次只做一件事(包含副程式),將一大段的程式碼依邏輯獨立性分拆成不同段落。實作前可先列出需進行工作內容(盡量詳細),再依工作間的相關性盡量將各工作獨立進行,使程式碼的結構在不顯著影響效能與保有正確性的前提下能更為單純簡潔。分離的形式並不重要,可以是抽離成新的副程式,也可以獨立成段,重點在於能確實分離。然而分離工作並非件容易的事,有時可能有五項工作,但囿於效能或其他原因使能將其分離出三項,但也是相當不錯的收穫了。


在希望增進可讀性之前,我們必須知道,可讀性最高的程式碼就是沒有任何程式碼。也就是說,一旦開始撰寫程式,就會立即面臨可讀性的問題。若想達到最佳可讀性,那程式碼自然是越自然、越短越好。要使程式碼更為自然,上述的抽離子問題與一次處裡一個問題的原則都可使讀者很自然地了解撰寫者的構思。但要使程式碼變短,並不能像存錢一樣,東省一兩行、西省一兩行,而是要更有組織,從結構上處理才是根本之道。因此必須掌握一個原則:不需要的功能不必開發。


經常在一個專案開始時,設計者都會很興奮地設想許多附加的功能,希冀這個完美的程式碼能夠具有無限擴充性,但往往反而會忽略專案真正的核心功能。最後這些附加的功能可能來不及完成,或根本沒人使用,反而增加程式的複雜度。因此想要縮短程式碼,必須從一開始設計時,就很清楚程式工作的核心目的,以及潛在的擴充方向,以完成核心部分為主要方向的前提下,再合理地維持擴充性。


開發過程中,隨著系統的成長,維持系統所需的複雜度會以等比級數增加。因此最好的方式是維持程式碼的小而美,也就是在開發過程中,盡量使用通用的副程式(適度的抽離子問題)以消除重複的程式碼,同時須果斷移除未使用的程式碼與功能,並隨時注意程式的份量。前者對大多數有觀念的撰寫者而言並非難事,但要求撰寫者自行刪除程式碼卻非易事。一旦程式碼被寫下之後,這些程式碼就代表工作的成果,刪除程式碼意味著之前所有努力全都白費,在心理上不易接受。當下定決心刪除程式碼後會發現,刪除獨立的函數相當容易,但有時無用的程式碼卻是散落在各個部分,不僅找尋不易,有時甚至連撰寫者都不知情。最常見的情況是程式設計之初,希望在核心功能外能搭配一些附帶功能,然而在撰寫過程中因專案目的改變或其他原因而放棄,但原先為此附帶功能而預留的空間或變數卻依然被保留,久而久之可能連設計者都忘記它們的存在,這些就是屬於無用的程式碼。


除了無用的程式碼需要刪除外,無用的變數也不需要保存。濫用變數或許能讓撰寫者快速地達到目的,但對讀者而言,變數越多越難同時記住所有變數;變數生存期越長,就必須記憶越久;變數若一直改變,就越難記得當前的數值。因此撰寫時必須刪除不必要的暫存變數,若該變數無法提供任何額外訊息,也無法簡化程式碼,就可考慮刪除此變數。在處理問題時,有時需要一些中介變數,使用這些中介變數時最好能盡量縮短它的生命週期,因此若能一次處理一件事情,並盡快完成所需的工作就能盡早刪除這些中介變數,避免這些中介變數成為潛在的bug。簡單的說,撰寫程式時,要盡量縮短變數的生命週期、減少變數出現的行數、也減少變數改變的次數,因為操作變數的地方越多就越難記得變數目前的數值。此外,也得避免使用過多的全域變數,以免全域變數汙染命名空間,使得修改區域變數的數值時,影響到全域變數的數值,產生一個潛在bug。


簡化的過程中除了依循上述三個概念外,有些不錯的小技巧也可幫助寫出簡潔的程式碼。例如在處理問題之前,先使用口語敘述遭遇的問題以及困難處,並說明可能的解決方法。如果發現無法用口語清楚描述,那表示可能遺漏某些東西,或仍有問題尚未被發掘。有時十分單純的問題,卻在實作過程中反而轉變為非常複雜的邏輯概念,通常遇到這種情況表示思考過程中遺漏一些重要因子,或其實有更簡單的作法只是還沒被想到。前者可透過口語敘述解決;後者則需具要些創造力找到更優雅的作法,一般可先嘗試能否從反向思考獲得解決方式。確定整體構想後,再依照構想寫下步驟並抽離子問題,養成這個習慣可幫助撰寫者在設計架構時能以更宏觀的角度思考並避免過度設計。

<參考資料>

易讀程式之美學-提升程式碼可讀性的簡單法則,2013,O'Reilly,ISBN:978-0-596-80229-5



2015年12月15日 星期二

[Linux] Screen

       撰寫程式時,經常會一邊執行(compile)程式碼,一邊繼續修改內容,有時還需要參考其他程式內容。因此,一次開個三四個執行視窗(putty/pietty)是很常見的事。但是使用這麼多個視窗,不僅切換相當不便,有時希望在螢幕上同時顯示數個視窗時,還需要花一些時間去調整式窗大小,相當不方便。在這種情況下,Screen就是一個相當好用的工具了。


       Screen 全名是Full-Screen Windows Manager,可以讓你在一個putty/pietty視窗下,同時開啟許多執行視窗,且視窗間的切換相當容易,也支援畫面分割(也就是可以在一個pietty視窗下,同時顯示兩個執行視窗)。這個有點像是Firefox或是chrome的分頁概念,但是使用上更方便。如此一來,就可以開一個執行視窗執行程式碼,然後再切換至第二個視窗撰寫程式碼,同時還能將畫面分割出第三個視窗,顯示想要參考的程式碼,再也不需花費額外的時間在切換pietty視窗,也不用擔心思路被打斷。


       除了能夠提供多視窗工作外,Screen更迷人的是允許離線背景作業,只要你離開時是使用detach離開,而不是exit,那麼在Screen模式下所開啟的所有視窗都可以被保留,若這些視窗中有正在執行的項目,也會在背景中繼續執行。對於經常在不同Project間來回穿梭的使用者來說,當Project A正在compile時,可以detach 當前的Screen,然後再開啟一個新的Screen處理Project B。除了背景執行功能可以省下等待的時間外,Screen還能保留在處理Project A時的環境,這樣再回頭處理Project A時,就能夠更快回憶起之前的進度。


     < Screen常用指令>
screen        :開啟screen功能,會直接跳到screen視窗中。
ctrl-a d        :detach screen,也就是暫時離開screen,screen會繼續再背景執行。
screen -r     : 喚醒被detach的screen。 -r 表示resume,如果只使用一個screen視窗,則直接用-r即可;若是已經開啟數個screen視窗的話,那後面就需要再加上screen 的ID。
screen -ls   :此指令會列出所有開啟的screen ID與該screen視窗是detached還是attached。
exit             :關閉當前的視窗,若只剩一個視窗,則離開screen。


       在screen中,所有操作都需伴隨Ctrl-a (C-a),如上述的ctrl-a d 就是指先同時按住ctrl 與 a鍵,放開後再按d鍵。下面這些都是screen模式中經常被使用的指令:


C-a c           :  開啟新的視窗,並同時移動至新開視窗。
C-a n或 C-a space                    :  切換至下一個視窗。
C-a p           :  切換至上一個視窗。
C-a C           :  清除當前視窗內容。


       除了上述這些常用熱鍵外,Screen 還有其他熱鍵,可以用C-a ? 查詢(或是直接man screen)。
雖然這些熱鍵並不複雜,但要一直按C-a還是有點麻煩,有時按太快還會讓電腦誤會(例如我之前習慣用C-a space來切換視窗,但有時按太快,反而變成切換輸入法了...),幸好,Screen有提供Bindkey(自訂熱鍵)的功能,只需要在安裝目錄下修改.screenrc即可。


       .screenrc的修改方式網路上有很多資源,而我主要是參考 [ 雅砌工坊 ] 所整理的配置,加上一些其他細部的調整,我覺得[ 雅砌工坊 ]  所提供的配置相當直接且方便,有興趣的歡迎自行前往參考。以下則是放上我所使用的配置(#號後面是註解):


# Start message (關掉screen的開啟訊息)
startup_message off
# Set hardstatus always on (在視窗底下新增狀態列)
hardstatus alwayslastline " %-Lw%{= Bw}%n%f %t%{-}%+Lw %=| %M %d %0c:%s "
(在狀態列中顯示系統日期與時間,Dec 15 13:33:21)此外,也有很多不同風格的狀態列可以使用,如


  • caption always "%{= wk} %{= KY} [%n]%t @ %H %{-} %= %{= KR} %l %{-} | %{= KG} %Y-%m-%d %{-} "
  • hardstatus alwayslastline " %-Lw%{= Bw}%n%f %t%{-}%+Lw %=| %0c:%s "
  • (除了顯示時間訊息外,也顯示load average的資訊)
  • caption always "%{=u .G} %-w%<%{=ub .y}%n %t%{=u .G}%+w "
  • hardstatus alwaysignore
  • hardstatus alwayslastline "%{= .K} [%l]%<%=%{= .W}@%H %=%{= .y} %Y/%m/%d%{= .m} %0c:%s "
  • (也是顯示時間與load average,只是配置風格不同)

# Set default encoding using utf8 ( 強制終端機編碼為utf8,這樣就可以看中文字了)
defutf8 on
# Refresh the display when exiting programs (離開視窗時重新整理螢幕)
altscreen ons
# Disable vbell (將操作熱鍵錯誤時的提示音關掉)
vbell off
# Scroll back (設定Scroll可以往上捲的行數)
defscrollback 10000


    <Keboard binding>
# bind F6 to list all screen windows (列出所有的screen 視窗)
bindkey -k k6 Windows
# bind F7 to detach screen session to background  (離開screen至背景,相當於C-a d)
bindkey -k k7 detach
# bind F8 to kill current screen window  (kill當前視窗,相當於exit)
bindkey -k k8 kill
# bind F9 to create a new screen (開新視窗,相當於C-a c)
bindkey -k k9 screen
# bind F10 to rename current screen window (重新命名當前視窗的標題)
bindkey -k k; title
# bind F11 to move to previous window (切換至前一個視窗,相當於C-a p)
bindkey -k F1 prev
# bind F12 to move to next window (切換至後一個視窗,相當於 C-a n)
bindkey -k F2 next
# bind ctrl-c to enter copy/scrollback mode  (切換至複製模式)
bindkey "^c" copy
# bind ctrl-v to paste the contents of the paste buffer (貼上複製範圍)
bindkey "^p" paste
       進入複製模式後,先使用方向鍵選取欲複製文字的開頭後,按下"空白鍵",此時會看到第一個字母已經反白。接著移動方向鍵選取欲複製的範圍,完成後按下大寫"Y",這樣screen就會將選取的內容存放到screen buffer,並且會在狀態列上顯示一共複製多少字母。 接著,便可利用ctrl-v 將暫存內容貼上。


      
       開頭時提到,Screen除了提供背景作業外或是多視窗工作外,也支援分割視窗。指令為:
ctrl-a S      :水平分割(上下)視窗,按幾次就分割幾個,分割後需要輸入欲顯示的視窗(例如利用F11、F12來選取)
ctrl-a Tab  :在不同的分割視窗間切換。
ctrl-a Q      :離開分割視窗。關閉所有的分割視窗,只留下目前focus的那一個。
ctrl-a X       :移除一個視窗。


同樣的,可以將分割視窗自訂熱鍵,並寫入.sceenrc中 ,如:
# bind alt-w to split the windows (水平分割視窗)
bindkey "^[w" split
# bind alt-v to vetically split the windows (垂直分割視窗)
bindkey "^[v" split -v
# bind alt-tab to switch to another split region (在不同分割視窗間切換)
bindkey "^[p" focus
# bind alt-q to quit delete all split regions but current focus one (關閉所有分割視窗,只留下當前focus的那個)
bindkey "^[q" only
(Note:  ^ 表示ctrl 鍵; ^[  表示 atl 鍵)


       此外, 由於screen中有許多預設的熱鍵,為避免不小心按到,網路上有一些建議remove調的指令,建議也可以放在.screenrc中,如:
# remove some default  key bindings
bind s   (避免不小心將螢幕鎖上)
bind k   (避免不小心kill視窗)
bind W
bind ^k
bind .
bind ^\
bind \\
bind ^h
bind h


<參考網站>






2015年11月17日 星期二

[NWP] 模式誤差_draft

模式誤差可以分為参數化誤差(或動力誤差)以及離散誤差。
前者主要來自於模式對真實大氣的描述能力不足,例如模式無法完整且正確地模擬出一朵積雲對流自生成到消散的過程,也無法完整表達積雲在對流過程中對環境的熱力與動力所造成的影響,這都會使數值模式在預報過程中產生誤差。
後者則是電腦計算過程中造成的誤差。由於數學上的微分方程都是連續的,若要在電腦上進行微分計算,必須將微分方程透過數值方法近似成差分方程,而在近似過程中會將高階項捨去;同時,電腦在計算時,受限於計算位數,也會將小數近似。這些數學方法近似過程中對模式造成的誤差稱作離散誤差。

2015年5月7日 星期四

[BashShell] 變數計算

Bash shell 的算術運算有四種方式。

1.  使用expr 
     ex: r=` expr 4 + 5 `
           r=` expr 4 \* 5 `

     note 1:  數字與符號間要有空白。
     note 2:  因為* 對於bash 來說有特殊意義(萬用字元),所以要先使用反斜線 (\) 來取消*的特殊意義。  
     note 3: expr無法使用(**)當作乘冪。
     note 4:expr 無法處理浮點運算,因此須改用其他方式,如$(())。
    
2.  使用 $(())
      ex: r=$(( 4 + 5))
            r=$(echo "4.0*2.0"|bc)  

       note 1:  此方式不需要使用反斜線來取消*的特殊意義。
       note 2:  亦可使用awk 來處理浮點運算,如:
           r=$(awk 'BEGIN{print 4.0*2.0 }')

            
3. 使用$[]
     ex: r=$[ 4 + 5 ]  

4. 使用let 
     ex: m=10
           let n=m+1
           則n=11
           (也可寫成let m=m+1 用來當作迴圈內計算)

NOTE:
 雖然上述四種方式都可以達到需求,但並非每個方式均可跨平台使用,建議使用expr會有較好的可攜性。


 

2014年9月3日 星期三

[Linux] 如何選擇壓縮格式&常見壓縮指令

在Linux中,有許多不同的壓縮格式。會存在許多的選擇意味著各種方法都各有優劣,也因此才會在科技洪流中被保存。當我們要決定使用哪一種壓縮格式前,我們必須思考下列幾項因素:

  1. 壓縮率(compression ratio, 壓縮後檔案/原始檔案)。
  2. 壓縮/解壓縮所需要的時間。
  3. 壓縮/解壓縮所需的記憶體。
  4. 在各系統上的相容性。
若僅比較常見的gzip和bzip這兩種壓縮格式,則:

  • gzip所需的計算時間和記憶體都比較小,若電腦資源較差,建議使用gzip;因為gzip是常見的壓縮格式,因此相容性也較佳,若要確認所有使用者均能順利解壓縮,gzip是首選。
  • bzip的壓縮率較佳,如果儲存空間相當吃緊時,建議使用bzip。

常見壓縮指令:


.tar
打包:tar cvf FileName.tar DirName
解包: tar xvf FileName.tar

.gz
壓縮:gzip FileName
解壓1:gunzip FileName.gz
解壓2:gzip -d FileName.gz

.tar.gz
壓縮:tar zcvf FileName.tar.gz DirName
解壓:tar zxvf FileName.tar.gz

.bz2
壓縮: bzip2 -z FileName
解壓1:bzip2 -d FileName.bz2
解壓2:bunzip2 FileName.bz2

.tar.bz2
壓縮:tar jcvf FileName.tar.bz2 DirName
解壓:tar jxvf FileName.tar.bz2


較不常見的壓縮指令:

.bz
壓縮:unkown
解壓1:bzip2 -d FileName.bz
解壓2:bunzip2 FileName.bz

.tar.bz
壓縮:unkown
解壓:tar jxvf FileName.tar.bz

.Z
壓縮:compress FileName
解壓:uncompress FileName.Z

.tar.Z
壓縮:tar Zcvf FileName.tar.Z DirName
解壓:tar Zxvf FileName.tar.Z

.tgz
壓縮:unkown
解壓:tar zxvf FileName.tgz


.tar.tgz
壓縮:tar zcvf FileName.tar.tgz FileName
解壓:tar zxvf FileName.tar.tgz

.zip
壓縮:zip FileName.zip DirName
解壓:unzip FileName.zip

.rar
壓縮:rar e FileName.rar
解壓:rar a FileName.rar

.lha
壓縮:lha -a FileName.lha FileName
解壓:lha -e FileName.lha