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 被迫实现 IOperational 和 ISelectable 接口,这将保证所有表面都会公开一组用于处理它们的基方法。IOperational 接口将强制覆盖类实现旋转和平移例程,而 ISelectable 接口将强制覆盖类实现 3D 选择例程。因此您会发现,通过实现这些接口,我们正在声明一种一致的工作方式。
这种思维方式的优点之一是代码可重用性清晰。例如,假设我们需要一种新的升力面,例如,VerticalEmpennage。如果我们没有将所有表面归类到一个共同的祖先之下,那么 Vertical empennage 应该从头开始生成,而无法从普通 LiftingSurface 或 Surface 的通用过程中获益。但通过让所有表面成为 Surface 的一种,那么我们可以像所有其他表面一样对待它。然后,VerticalEmpennage 可以从 LiftingSurface 派生,实现其所有属性和方法,但随后提供一个更面向尾翼属性的接口,例如方向舵的配置。
面向对象技术提供的这些工具实际上是为了让您的代码像机器一样工作。如果您的生活兴趣偏向于机械,那么也许编程不适合您。因此,为了从一开始就把事情做好,有一个基本的编码原则您应该牢记在心。编程与构建硬件机器没有太大区别。您通过组装专门用途的单元来构建一台成功的机器。这些部件中的每一个都有内部状态变量,这些变量不会直接与外部世界交互。组件之间的交互是通过公开一个 可见 接口来实现的,该接口隐藏了子组件的内部复杂性,并且只显示连接部分。在编程中,这称为 封装。在物质世界中,我们称之为 盖子 或 面板。例如,考虑一个电气控制面板。从外部看,它只呈现一组按钮,因此面板的用户将能够通过与它们交互来控制系统,并且他们永远不会接触到内部的连接。.NET 中的封装是通过在字段、属性或方法中声明私有或公共标志来引入的。公共声明会给您的对象或模块一个外部外观,使功能代码保持隐藏和受保护。
首先,OpenVOGEL 将代码分为两个不同的分支:计算模型和可视化模型。当你使用 HMI(人机交互界面)操作项目时,你实际上是在处理可视化模型。当你启动计算时,这个模型会内部转换为计算模型,进行分析,然后转换回另一个可视化模型以进行后处理。这样做原因很简单:计算模型不需要知道如何表示你的 3D 模型,而可视化模型不需要知道结果是如何产生的。此外,计算模型必须处理性能问题,并且不需要了解几何体的生成细节。所以,这种划分是为了“在每一方保留严格必要的內容”。否则,我们就会不断地使用无关数据进行处理,在构建一个复杂的程序时,这会导致混乱、困惑和错误。
在下一节中,我们将描述 OpenVOGEL.DesignTools 中包含的可视化模型结构。OpenVOGEL.AeroTools 中的计算模型更为复杂,因为它处理空气动力学和结构算法,因此它将成为专门章节的主题。如果你迫不及待地想了解它,可以点击这里。
可视化模型有两个目标:收集输入数据和向用户展示结果。因此,可视化模型自然地分为两个部分:DesignModel 和 ResultModel。通过 DesignModel,你将面对一系列工具来声明你希望模型如何构建。当你运行任何模拟时,DesignModel 会被转换为计算对象,并在模拟过程中将结果文件写入硬盘。分析结束后,这些文件会被读取并转换为 ResultModel,它会向你展示结果。ResultModel 包含原始设置和一系列 ResultFrames(取决于模拟类型)。
可视化模型允许你通过组合四种不同类型的对象来创建一个飞机
- LiftingSurface
- Fuselage
- JetEngine
- ImportedSurface
如前所述,这四个类继承了名为 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,并将对象直接在代码中实现。
现在你已经了解了模型的结构,你可能想知道 DesignModel 和 ResultModel 实际位于哪里。好吧,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 文件可以在文本编辑器中进行一些修改后导入。输入文件仍然没有标准,但源代码始终可以作为参考。
模型在 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 容器中表示过渡状态(动画)。