跳轉至內容

OpenVOGEL/原始碼

來自華夏公益教科書,開放的世界,開放的書籍

原始碼

[編輯 | 編輯原始碼]

正如引言中所述,OpenVOGEL 是用 .NET 框架編寫的,主要使用 Visual Basics。已經連結了用 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(取決於模擬型別)。

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

  • 升力面
  • 機身
  • 噴氣發動機
  • 匯入面
這三種標準表面型別(機翼、機身和發動機)組合在一起,代表了飛機模型。
結果顯示在 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 容器中表示過渡狀態(動畫)。
華夏公益教科書