跳轉到內容

OpenVOGEL/原始碼

來自華夏公益教科書

原始碼

[編輯 | 編輯原始碼]

正如引言中所述,OpenVOGEL 是用 .NET 框架編寫的,主要使用 Visual Basic。已連結用 C# 編寫的外部庫,但它們的開發不在本專案的範圍之內,因為它們幾乎保持不變。我知道,理解其他人編寫的原始碼並不總是容易的,因此在本章中,我將盡力用盡可能清晰的語言來解釋所有這些是如何組織的。如果您有 FORTRAN 程式碼(例如 PanAir)的經驗,並且想要學習 OpenVOGEL,請注意,面向物件的程式設計採用了一種截然不同的方法。希望,閱讀完本文後,您將能夠找到程式碼中的路徑,並按照您的意願進行自己的調整。

在 OpenVOGEL 中,程式碼分佈在不同的庫中,並且有一個邏輯來解釋這一點。要理解庫的結構及其關係,請檢視下圖。

整體架構

所有低階數學程式都位於 OpenVOGEL.DotNumerics 中。這是一個由 Jose Antonio De Santiago Castillo 建立的 DotNumerics 專案的分支,它基本上是 LAPACK 和 BLAS 從 FORTRAN 到 C# 的自動翻譯。在這個專案中,我添加了 子空間迭代 方法(Bathe),這是一種非常具體的演算法,適用於在 Mv=aKv 型別的系統中找到最低特徵值和特徵向量,該系統具有大量自由度。此外,我在庫中添加了對 Intel MKL 的繫結,可以選擇使用它來提高計算效能,而不是使用本地 .NET 例程。但是,這些繫結保留了 DotNumerics 高階 API。因此,DotNumerics 主要用於透過 LU 分解求解線性方程組,以及找到機翼的振動模式。

OpenVOGEL 也有自己的線性代數包,它儲存在名為 OpenVOGEL.Math 的庫中。在這裡,我引入了向量、數值積分和其他對氣動、動力和結構演算法必不可少的實用物件。為了瞭解這有多重要,請想想專案中的所有向量都是位於 OpenVOGEL.Math.EuclideanSpace 中的 Vector2 或 Vector3 類。

實際的勢流和氣動彈性求解器包含在 OpenVOGEL.AeroTools 庫中。計算核心只使用前面提到的庫,並且包含可以嵌入任何外部專案中的通用定義,而無需 Tucan 或 Console。

除了計算庫之外,該專案還提供了建模工具,可以生成飛機模型的網格,方法是通過幾何形狀的引數化描述。這包含在 OpenVOGEL.DesignTools 庫中。該庫還提供從設計模型到計算模型以及反之的轉換過程。同樣,該庫可以嵌入任何外部專案,而無需 Tucan 或 Console。

OpenVOGEL.Console 專案是一個控制檯應用程式,可以輕鬆地在 Windows 或 Linux(使用 Mono)上執行。該控制檯能夠讀取、計算和寫入任何型別的 OpenVOGEL 專案,就像 Tucan 一樣(即呼叫完全相同的過程),但沒有圖形介面。此應用程式是為能夠自行進行預處理和後處理的進階使用者提供的。例如,您可以使用它來執行批處理分析並執行您自己的自定義分析。

最後,OpenVOGEL.Tucan 是我們面向公眾的主要解決方案。它依賴於 System.Drawing、Winforms 和 OpenGL 來生成資料的圖形表示。此解決方案目前僅適用於 Windows(7 和 10)。

現在您瞭解了專案的架構,是時候看看原始碼了。要理解原始碼,您首先需要了解面向物件以及 .NET 如何實現它,因為大部分程式碼都是這樣編寫的。如果您是新手,那麼我建議您先閱讀一些專門的書籍(有很多),以及微軟提供的 .NET 文件,它非常豐富。

簡而言之,面向物件程式碼與過程式程式碼之間的區別在於,在面向物件程式碼中,我們更多地關注系統如何劃分成功能元件,以及這些元件如何作為一個整體協同工作。另一方面,在過程式程式碼中,您更多地關注系統為實現目標而執行的不同操作。然後,資料在物件內部駐留,而不是從一個例程傳遞到另一個例程。

在面向物件程式設計中,您將把系統分解成幾種型別的稱為 的小元件,然後根據需要例項化這些類來建立儘可能多的元件以構建您的程式集。每個 都將資料封裝成 欄位屬性 的形式,並能夠透過 函式過程 執行操作,也稱為 方法(請記住所有這些概念,因為它們構成了面向物件的精髓)。此外,共享某些屬性或過程的元件可以構建為繼承一個共同的祖先,這稱為 繼承。為了更清楚地說明,讓我舉一個現實世界的例子。在 OpenVOGEL 中,我們需要對飛機進行建模,飛機基本上由多個機翼、機身和發動機艙組成。但是,很明顯,這三個元件實際上都是某種表面,因此它們實際上共享一組共同的屬性。它們都有一個網格,並且它們都可以被選中、移動、旋轉並寫入檔案。因此,無論它們是什麼型別的表面,它們都繼承了一個稱為 Surface 的原始類。

Public MustInherit Class Surface 
   Implements IOperational 
   Implements ISelectable
   [...]
End Class

如您所見,Surface 被迫實現 IOperationalISelectable 介面,這將保證所有表面都會公開一組用於處理它們的基方法。IOperational 介面將強制覆蓋類實現旋轉和平移例程,而 ISelectable 介面將強制覆蓋類實現 3D 選擇例程。因此您會發現,透過實現這些介面,我們正在宣告一種一致的工作方式。

這種思維方式的優點之一是程式碼可重用性清晰。例如,假設我們需要一種新的升力面,例如,VerticalEmpennage。如果我們沒有將所有表面歸類到一個共同的祖先之下,那麼 Vertical empennage 應該從頭開始生成,而無法從普通 LiftingSurfaceSurface 的通用過程中獲益。但透過讓所有表面成為 Surface 的一種,那麼我們可以像所有其他表面一樣對待它。然後,VerticalEmpennage 可以從 LiftingSurface 派生,實現其所有屬性和方法,但隨後提供一個更面向尾翼屬性的介面,例如方向舵的配置。

基本編碼原則

[編輯 | 編輯原始碼]

面向物件技術提供的這些工具實際上是為了讓您的程式碼像機器一樣工作。如果您的生活興趣偏向於機械,那麼也許程式設計不適合您。因此,為了從一開始就把事情做好,有一個基本的編碼原則您應該牢記在心。程式設計與構建硬體機器沒有太大區別。您透過組裝專門用途的單元來構建一臺成功的機器。這些部件中的每一個都有內部狀態變數,這些變數不會直接與外部世界互動。元件之間的互動是透過公開一個 可見 介面來實現的,該介面隱藏了子元件的內部複雜性,並且只顯示連線部分。在程式設計中,這稱為 封裝。在物質世界中,我們稱之為 蓋子面板。例如,考慮一個電氣控制面板。從外部看,它只呈現一組按鈕,因此面板的使用者將能夠透過與它們互動來控制系統,並且他們永遠不會接觸到內部的連線。.NET 中的封裝是透過在欄位、屬性或方法中宣告私有或公共標誌來引入的。公共宣告會給您的物件或模組一個外部外觀,使功能程式碼保持隱藏和受保護。

程式碼結構

[編輯 | 編輯原始碼]

首先,OpenVOGEL 將程式碼分為兩個不同的分支:計算模型和視覺化模型。當你使用 HMI(人機互動介面)操作專案時,你實際上是在處理視覺化模型。當你啟動計算時,這個模型會內部轉換為計算模型,進行分析,然後轉換回另一個視覺化模型以進行後處理。這樣做原因很簡單:計算模型不需要知道如何表示你的 3D 模型,而視覺化模型不需要知道結果是如何產生的。此外,計算模型必須處理效能問題,並且不需要了解幾何體的生成細節。所以,這種劃分是為了“在每一方保留嚴格必要的內容”。否則,我們就會不斷地使用無關資料進行處理,在構建一個複雜的程式時,這會導致混亂、困惑和錯誤。

在下一節中,我們將描述 OpenVOGEL.DesignTools 中包含的視覺化模型結構。OpenVOGEL.AeroTools 中的計算模型更為複雜,因為它處理空氣動力學和結構演算法,因此它將成為專門章節的主題。如果你迫不及待地想了解它,可以點選這裡

視覺化模型

[編輯 | 編輯原始碼]

視覺化模型有兩個目標:收集輸入資料和向用戶展示結果。因此,視覺化模型自然地分為兩個部分:DesignModelResultModel。透過 DesignModel,你將面對一系列工具來宣告你希望模型如何構建。當你執行任何模擬時,DesignModel 會被轉換為計算物件,並在模擬過程中將結果檔案寫入硬碟。分析結束後,這些檔案會被讀取並轉換為 ResultModel,它會向你展示結果。ResultModel 包含原始設定和一系列 ResultFrames(取決於模擬型別)。

視覺化模型允許你透過組合四種不同型別的物件來建立一個飛機

  • LiftingSurface
  • Fuselage
  • JetEngine
  • ImportedSurface
這三種標準的表面型別(機翼、機身和發動機)組合在一起,代表了一個飛機模型。
結果將在 ResultModel 中顯示

如前所述,這四個類繼承了名為 Surface 的公共祖先。DesignModel 將所有表面儲存在 List(Of Surface) 中,並提供公共方法將每個特定型別新增到堆中。

Public Class DesignModel
   [...]
   Public Property Objects As New List(Of Surface)
   Public Sub AddLiftingSurface()
   Public Sub AddExtrudedBody()
   Public Sub AddJetEngine()
   [...]
End Class

由於所有表面都被平等對待,因此你可以輕鬆地將自己的型別新增到軟體中。基本上,你只需要建立一個新的 Surface 後代型別,並在 DesignModel 上編寫一個方法將其載入到堆中。

Public Class MyNewSurfaceType 
   Inherits Surface 
   [...]
End Class

Public Class DesignModel
   [...]
   Public Sub AddMySurfaceType() 
      Dim NewSurface = New MyNewSurfaceType
      NewSurface.Name = String.Format("Surface - {0}", Objects.Count) 
      Objects.Add(NewSurface) 
   End Sub 
   [...]
End Class

當然,如果你希望它支援 HMI,還需要建立一個使用者控制元件,但上面的程式碼片段將實現基本技巧。如果只在控制檯專案中工作,你肯定可以省略 HMI,並將物件直接在程式碼中實現。

專案根目錄

[編輯 | 編輯原始碼]

現在你已經瞭解了模型的結構,你可能想知道 DesignModelResultModel 實際位於哪裡。好吧,OpenVOGEL 將它們儲存在 ProjectRoot 靜態模組上,該模組位於 DesignTools/DataStore 目錄中。你可以在整個應用程式中訪問此模組。ProjectRoot 不僅提供對大部分程式資料的訪問,還包含管理三種不同使用者介面模式的邏輯:設計、計算設定和結果後處理。它透過公開一組公共呼叫來實現這一點

  • 用於同步計算啟動和結果載入的呼叫
  • 用於 OpenVOGEL (*.vog) 檔案的輸入/輸出呼叫。

所以你在 HMI 上做的很多事情,實際上都是在這裡處理的。

Public Module ProjectRoot
   Public Property Name As String = "New aircraft"
   Public Property FilePath As String = "" 
   Public Property SimulationSettings As New SimulationSettings 
   Public Property Model As DesignModel 
   Public Property Results As New ResultModel 
   Public Property VelocityPlane As New VelocityPlane 
   Public Property CalculationCore As Solver
   [...]
End Module

現在讓我們仔細看看軟體當前提供的三個標準組件。你可能已經注意到,它們之間最大的區別在於網格的建立方式,而不是處理方式。它們都公開了一組特殊的引數屬性,這些屬性旨在生成特定型別的網格,它們透過覆蓋從 Surface 繼承的 GenerateMesh 方法來實現這一點。

Public MustInherit Class Surface 
   [...]
   Public Overridable Sub GenerateMesh()
   [...]
End Class

Public Class LiftingSurface 
   Inherits Surface 
   [...]
   Public Overrides Sub GenerateMesh()
      [...]
   End Sub
End Class
升力面
[編輯 | 編輯原始碼]

可能最受歡迎的型別是 LiftingSurface 類。此表面型別配備了特殊的網格劃分演算法,該演算法允許你透過宣告一排相鄰的 宏面板(由 WingRegion 類表示)來模擬細長的機翼。此類收集描述機翼單箇中間區域所需的所有屬性,例如翼尖弦長、長度和後掠角。它還包含定義區域性網格作為網格的引數:跨度方向和絃方向的面板數量。

Public Class WingRegion
   [...]
   Public Property SpanPanelsCount As Integer
   Public Property ChordNodesCount As Integer
   Public Property TipChord As Double
   Public Property Length As Double
   Public Property Sweepback As Double
   [...]
End Class

Public Class LiftingSurface  
   [...]
   Public Property WingRegions As New List(Of WingRegion)
   [...]
End Class

機身型別具有完全不同的網格劃分技術。OpenVOGEL 中的機身是一種由一組縱向橫截面定義的放樣表面,這些橫截面被插值以定位網格節點。

然而,機身的真正複雜性不在於此,而在於機翼錨點的必要性。這些特徵在面板法中是必要的,以提供迴圈的連續性並避免洩漏。在生成網格節點之前,網格劃分演算法必須掃描所有連線的機翼,並透過將機翼根節點投影到原始機身表面上生成錨線。一旦錨點就緒,表面就會以縱向塊進行網格劃分。如果塊包含錨點,則網格節點將被強制保持介面點的位置。

機翼錨點是使用區域性縱向軸與全域性 X 軸重合生成的。這種限制使錨點演算法更容易,但它與網格劃分後的變換(平移/旋轉/平移)不相容。因此,打算錨定的機身不應該旋轉或平移。

連線升力面和機身的錨線的示例。
Public Class Fuselage
   Inherits Surface
   [...]
   Public Property CrossSections As List(Of CrossSection)
   Public Property AnchorLines As List(Of AnchorLine)
   Public Property MeshType As MeshTypes = MeshTypes.StructuredQuadrilaterals
   [...]
End Class
End Class
發動機短艙
[編輯 | 編輯原始碼]

簡而言之,發動機短艙只是薄壁管。這些表面提供了一個選項,可以在其後緣處脫落封閉的尾流,因此它們可以用來分析噴氣發動機或風扇管道。這對於瞭解升力如何從機翼傳遞到發動機特別有用。

匯入的表面
[編輯 | 編輯原始碼]

匯入的表面是唯一一種不是以引數方式建立的表面。它們不是在內部建立的,而是從手動建立或由第三方程式建立的檔案中讀取的。該檔案僅在編輯表面時讀取,然後在內部儲存以供進一步使用。原始網格在儲存專案時儲存在專案 XML 檔案中,因此當重新開啟模型時,不再需要原始檔案。

這些表面是我們努力與其他軟體實現互操作性的結果。目前,APAME 檔案可以在文字編輯器中進行一些修改後匯入。輸入檔案仍然沒有標準,但原始碼始終可以作為參考。

模型在 OpenGL 中的 3D 表示

[編輯 | 編輯原始碼]

模型在 Tucan 中的表示是透過 OpenGL 呼叫完成的。由於歷史原因,OpenVOGEL 目前僅使用相容模式呼叫(即 OpenGL 1.4),而不是核心配置檔案。如本章開頭所述,OpenGL 透過 Dave Kerr 用 C# 編寫的 SharpGL 繫結與專案連結。該專案不僅提供了必要的 OpenGL 上下文,還提供了一個 Winforms 小部件,它在 Winforms 應用程式中顯示結果畫素緩衝區,並公開相關的繪圖事件。

通常情況下,人們會期望在不同的類中直接找到繪圖過程。然而,由於專案的理念是為不同目的提供獨立的庫,因此 OpenGL 依賴項已放在 Tucan 本身(唯一包含 HMI 的專案)中。AeroTools 和 DesignTools 都沒有引用 SharpGL。繪圖過程已放在一個單獨的單元中,並作為父類的擴充套件編寫。透過新增額外的過程來擴充套件類是一個非常有用且易於實現的 .NET 功能。

渲染過程可以在 OpenVOGEL.Tucan.Utility.ModelRendering.vb 中找到。有一個可以用於任何表面的排程過程,以及針對每種表面型別的特定過程。

Module ModelRendering
    ''' General extension that redispatches the rendering method to the correct surface:
    <Extension()>
    Public Sub Refresh3DModel(This As Surface,
                              ByRef gl As OpenGL,
                              Optional ByVal ForSelection As Boolean = False,
                              Optional ByVal ElementIndex As Integer = 0)
        If TypeOf This Is LiftingSurface Then
            Dim Surface As LiftingSurface = This
            Refresh3DModel(Surface, gl, ForSelection, ElementIndex)
        ElseIf TypeOf This Is Fuselage Then
            Dim Surface As Fuselage = This
            Refresh3DModel(Surface, gl, ForSelection, ElementIndex)
        ElseIf TypeOf This Is JetEngine Then
            Dim Surface As JetEngine = This
            Refresh3DModel(Surface, gl, ForSelection, ElementIndex)
        ElseIf TypeOf This Is ImportedSurface Then
            Dim Surface As ImportedSurface = This
            Refresh3DModel(Surface, gl, ForSelection, ElementIndex)
        ElseIf TypeOf This Is ResultContainer Then
            Dim Surface As Fuselage = This
            Refresh3DModel(Surface, gl, ForSelection, ElementIndex)
        End If
    End Sub
    ...
    ''' All the extensions for the particular classes:
    <Extension()>
    Public Sub Refresh3DModel(This As LiftingSurface,
                              ByRef gl As OpenGL,
                              Optional ByVal ForSelection As Boolean = False,
                              Optional ByVal ElementIndex As Integer = 0)
    ...
    End Sub
    ...
...
End Module

實際的 OpenGL 訊號包含在 OpenVOGEL.Tucan.Utility.ModelInterface.vb 中。訊號包括:

  • 當模型發生變化時,為每個表面重建 OpenGL 列表(OpenGL 1.4)。
  • 當 3D 容器請求重繪時,使用列表和當前相機設定重新整理模型。
  • 當用戶點選 3D 容器時,使用 OpenGL 點選處理元件的選擇。
  • 在 3D 容器中表示過渡狀態(動畫)。
華夏公益教科書