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 被迫实现接口 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(取决于模拟类型)。
可视化模型允许您通过组合四种不同类型的对象来创建一个飞机
- 升力面
- 机身
- 喷气发动机
- 导入面
如前所述,这四个类继承了名为 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 容器中表示过渡状态(动画)。