OpenVOGEL/SourceCode

Source codeEdit

IntroductionEdit

As it was said in the introduction, OpenVOGEL has been programmed in the .NET frameworks, mostly using Visual Basics. External libraries written in C# have been linked, but their development is out of the scope of this project, since they have remained almost untouched. I am conscious that it is not always easy to understand the source code written by someone else, so in this chapter I will try to make an effort to explain in words and as clear as possible how it all has been organized. If you had experience in FORTRAN code (such as PanAir) and you want to learn OpenVOGEL, take into account that object oriented programming consists in a very different approach. Hopefully, after reading this you will be able to find the way through the code and make your own adaptations the way you like.

LibrariesEdit

In OpenVOGEL the code is distributed in different libraries, and there is a logic for this. To understand the structure of libraries and their relationship, follow the diagram here next.

 
Architecture at large

All low level mathematical procedures are located in OpenVOGEL.DotNumerics. This is a fork of the DotNumerics project created by Jose Antonio De Santiago Castillo, which is basically an automatic translation of LAPACK and BLAS from FORTRAN to C#. In this project I have added the Subspace Iteration method (Bathe), which is a very specific algorithm suitable for finding the lowest eigen-values and vectors in systems of the type Mv=aKv with a large number of degrees of freedom. Additionally, I have added to the library bindings to the Intel MKL that can be used optionally for improved calculation performance instead of the native .NET routines. These bindings retain however the DotNumerics high level API. DotNumerics is thus basically used for solving the system of linear equations by LU decomposition, and to find the vibration modes of the wings.

OpenVOGEL has also its own linear algebra package, which is stored in the library called OpenVOGEL.Math. Here I have introduced vectors, numeric integration and other useful objects that are essential for the aerodynamic, dynamic and structural algorithms. To have an idea of how important this is, think that all vectors in the project are either Vector2 or Vector3 classes residing in OpenVOGEL.Math.EuclideanSpace.

The actual potential flow and aeroelastic solver is contained in the OpenVOGEL.AeroTools library. The calculation core only uses the previously mentioned libraries, and contains general definitions that can be embedded in any external project without the necessity of the Tucan or the Console.

In addition to calculation libraries the project also offers modeling tools that can generate the mesh of airplane models through a parametric description of the geometry. This is contained in the OpenVOGEL.DesignTools library. This library also offers the conversion procedures that are necessary to go from the design model to the calculation model and vice versa. Again, this library can be embedded in any external project without requiring Tucan or the Console.

The OpenVOGEL.Console project is a console application that can easily run in Windows or Linux (using Mono). This console is able to read, calculate and write any kind of OpenVOGEL project, just as Tucan does it (i.e. calling exactly the same procedures), but without graphical interface. This application is offered for advanced users that are able to do the pre- and post-processing by themselves. You can use it, for instance, to run a batch analysis and to perform your own customized analysis.

Finally, OpenVOGEL.Tucan is our main solution targeted for the general public. It relies on System.Drawing, Winforms and OpenGL to generate a graphical representation of the data. This solution is currently only available for Windows (7 and 10).

ClassesEdit

Now you understand the architecture of the project, it is time to take a look at the source code. To understand the source code, you first need to know about object orientation and how .NET implements it, since most of the code has been written that way. If you are new to this, then I would recommend you start by reading some dedicated book (there are many of them), along with the documentation of .NET provided by Microsoft, which is very rich.

In short, the difference between object orientation and procedural code is that in the former style we focus more our attention in how the system is divided in functional components, and how these components work together as an assembly. In procedural code, on the other hand, you would focus more on the different actions that a system performs to reach a goal. The data is then passed from one routine to another in place of residing inside an object.

In object oriented programming you would dismantle your system into several types of small components called classes, and then instantiate these classes in as many components as you need to build your assembly. Each class encapsulates data in the form of fields and properties, and is able to perform actions through functions and procedures, also called methods (try to remember all these concepts, since they constitute the soul of object orientation). Moreover, components that share some properties or procedures can be built to inherit a common ancestor, something that is called inheritance. For more clarity, let me put this idea in a down-to-earth example. In OpenVOGEL we need to model airplanes, which are basically made of several wings, fuselajes and nacelles. However, it is clear that all these three components are in fact some kind of surface, so they share in fact a set of common properties. They all have a mesh, and they all can be selected, moved, rotated and written into a file. So independently on what kind of surface they are, they all inherit a primitive class called Surface.

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

As you can see, Surface is forced to implement the interfaces IOperational, and ISelectable, which will guarantee that all surfaces will expose a set of basic methods for handling them. The interface IOperational will force the overriding class to implement rotation and translation routines, and the ISelectable interface will force the overriding class to implement 3D selection routines. So you see that by implementing these interfaces we are declaring a consistent way of working.

Among the advantages of this way of thinking is clarity in code reusability. For example, lets imagine that we need a new kind of lifting surface, lets say, a VerticalEmpennage. If we would not have classified all our surfaces under a common ancestor, then Vertical empennage should be generated from scratch, without profiting from the general procedures of a normal LiftingSurface, or a Surface. But by letting all surfaces be a kind of Surface, then we can threat it exactly the same way as all others. VerticalEmpennage could then be derived from LiftingSurface, implement all its properties and methods, but then provide an interface more oriented towards the properties of an empennage, like for instance the configuration of the rudder.

Basic coding principlesEdit

All these tools provided by the object oriented technique are in fact meant to make your code work as a machine. If your interest in life diverges to mechanics, then maybe programming is not your forte. So to get the things right from the beginning, there is a fundamental principle of coding that you should keep in mind. Programming is not very different from building a hardware machine. A successful machine you build by assembling units that are meant for a specific purpose. Each of these parts have internal state variables that do not interact directly with the outside world. The interaction between components is made by exposing a visible interface that hides the internal complexity of the subassembly, and only displays linking parts. In programming this is called encapsulation. In a material world we call this a cover, or a panel. As an example consider an electricity panel. From the outside it only presents a groups of buttons, so the users of the panel will be able to control the system by interacting with them, and they will never get to play with the connections inside. Encapsulation in .NET is introduced by declaring either private or public flags in fields, properties or methods. Public declarations give your objects or modules an outside look, keeping the functional code hidden and protected.

Structure of the codeEdit

Lets start by saying that OpenVOGEL divides the code into two different branches: the calculation model, and the visual model. When you work with your project from the HMI (human machine interface), you are actually dealing with the visual model. When you launch a calculation, this model is internally converted into a calculation model, analysed and the converted back into another visual model for post processing. The reason why this has been done is simple: the calculation model does not need to know about how you represent your 3D model, and the visual model does not need to know nothing about how the results came out. Also, the calculation model has to deal with performance issues and does not need to know the details about how the geometry was generated. So the division has to do with "keeping on each side what is strictly necessary". Otherwise we would be bouncing all the time with irrelevant data, and when building a complex program, this can mean chaos, confusion and bugs.

In the next section we will describe the structure of the visual model contained in OpenVOGEL.DesignTools. The calculation model in OpenVOGEL.AeroTools is more complex because it deals with the aerodynamic and structural algorithms, so it will be subject of a dedicated section. If you can't way to see it, then navigate here.

Visual modelEdit

The visual model has two goals: gathering input data and showing the results to the user. So the visual model is naturally divided in two parts: the DesignModel and the ResultModel. Through the DesignModel you are confronted with a set of tools to declare how you want your model to be. When you run any simulation, the DesignModel is converted into a calculation object, and during the simulation results files are written to the hard disk. After the analysis, these files are read and translated into a ResultModel which confronts you with the results. The ResultModel contains the original settings and a collection of ResultFrames (depending on the simulation type).

The visual model lets you create an airplane by assembling together a number of four different kind of objects:

  • LiftingSurface
  • Fuselage
  • JetEngine
  • ImportedSurface
 
The three standard surface types (wings, fuselage and engines) assembled together to represent an airplane model.
 
The results are displayed in a ResultModel

As said before, these four classes inherit a common ancestor called Surface. The DesignModel stores all the surfaces in a List(Of Surface), and provides public methods to add every particular type into the heap.

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

Because of the surfaces are all treated equally, you could easily add your own types to the software. Basically you would just create a new Surface descendant type, and write a method on the DesignModel to load it on the heap.

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

Of course you would also need to make a user control if you want HMI support for it, but the above code snippet would make the basic trick. If only working in the console project, you would definitely omit the HMI and simply implement the object directly in the code.

Project rootEdit

Now you know how the models are structured, you are probably wondering where the DesignModel and the ResultModel actually reside. Well, OpenVOGEL stores them on the ProjectRoot static module, which is located in the DesignTools/DataStore directory. You can access this module throughout the whole application. ProjectRoot provides not only access to most of the program data, but it also contains the logic that manages the three different user interface modes: design, calculation setup and post-processing of results. It does this by exposing a set of public calls:

  • Calls for synchronous calculation startup and loading of results
  • Calls for input/output of OpenVOGEL (*.vog) files.

So much of what you do on the HMI, is actually handled here.

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

ComponentsEdit

Lets now take a closer look at the three standard components the software currently provides. You have probably already noticed that the greatest difference between them resides in the way the mesh is created, rather than how the are handled. They all expose a set of special parametric properties that are intended to generate a particular kind of mesh, and they do this by overriding the GenerateMesh method that they inherit from Surface.

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

Public Class LiftingSurface 
   Inherits Surface 
   [...]
   Public Overrides Sub GenerateMesh()
      [...]
   End Sub
End Class
Lifting surfacesEdit

Probably the most popular type is the LiftingSurface class. This surface type has been equipped with a special meshing algorithm that lets you model slender wings by declaring a row of adjacent macro panels represented by the class WingRegion. This class gathers all of the properties that are necessary to describe a single intermediate region of the wing, like the tip chord, the length and the sweepback angle. It also contains the parameters that define the local mesh as a grid: the number of span-wise and chord-wise panels.

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
FuselagesEdit

The fuselage type has a radically different meshing technique. Fuselages in OpenVOGEL are a sort of lofted surfaces defined from a set of longitudinal cross sections that are interpolated to locate the mesh nodes.

The real complexity of the fuselages, however, does not reside here but in the necessity of wing anchors. These features are needed in the panel method to provide continuity in the circulation and to avoid leakage. Before generating the mesh nodes, the meshing algorithm has to scan all connected wings and generate the anchor lines by projecting the wing root nodes on the primitive fuselage surface. Once the anchors are ready the surface is meshed in longitudinal chunks. If a chunk contains an anchor the mesh nodes are forced to keep the interface points in position.

The wing anchors are generated with the local longitudinal axis coincident to the global X axis. This limitation makes the anchoring algorithm easier, but it is not compatible with post meshing transformations (translation/rotation/translation). Therefore, fuselages that are meant to be anchored should not be rotated or translated.

 
Example of anchor lines joining lifting surfaces to the fuselage.
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
NacellesEdit

In short, nacelles are simply thin wall tubes. These surfaces offer an option to shed a closed wake from their trailing edge, so they can be useful to analyse jet engines or fan-ducts. This is specially interesting to have an idea of how the lift is transferred from the wing to the engine.

Imported surfacesEdit

Imported surfaces are the only kind of surface that are not created in a parametric way. They are not created internally, but just read from a file that has been created manually or by a third party program. The file is only read when editing the surface and then stored internally for further use. The original mesh is kept in the project XML file when saving the project, so that when the model is reopened the original file is no longer required.

These surfaces are part of our effort to create interoperability with other software. At the moment, APAME files can be imported after some modifications in a text editor. There is still no standard for the input files, but the source code can always be used as reference.

3D representation of the models in OpenGLEdit

The representation of the models in Tucan is done through OpenGL calls. Due to historical reasons, OpenVOGEL curretly only uses compatibility mode calls (i.e. OpenGL 1.4) instead of Core Profile. As explained at the beginning of this chapter, OpenGL is linked to the project through the SharpGL binding written in C# by Dave Kerr. That project does not only provide the necessary OpenGL context, but also a Winforms widget that displays the resulting pixel buffer in a winforms application and exposes the related drawing events.

Normally one would expect to find the drawing procedures directly inside the different clases. However, because the idea of the project was to provide independent libraries for different purposes, the OpenGL dependencies have been located in Tucan itself (the only project with HMI). Neither AeroTools nor DesignTools contain reference to SharpGL. The drawing procedures have been located inside a single unit and written as extensions to the parent classes. Extending a class by adding extra procedures is a quite useful and easy to implement .NET feature.

The rendering procedures can be found in OpenVOGEL.Tucan.Utility.ModelRendering.vb. There is a dispatching procedure that can be used for any surface, and a particular procedure for each surface kind.

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

The actual OpenGL signals are contained in OpenVOGEL.Tucan.Utility.ModelInterface.vb. The signals include:

  • Rebuilding the OpenGL lists (OpenGL 1.4) for each surface when the model has changed.
  • Refreshing the model using the lists and the current camera setup when the 3D container requests a redrawing.
  • Handling the selection of the components using the OpenGL hits when the user clicks on the 3D container.
  • Representing the transit state (animation) in the 3D container.