Electronics, Embedded Systems, and Software are my breakfast, lunch, and dinner.
Dec 27, 2012
After redoing this project several times, I have finally managed to get it working in C++. I gave up for the moment on the GUI since I figured it would be best to get the back end model working first before tackling the GUI. The biggest change I made from my previous paradigm is that I decided to split this into several Qt projects. The model is held in a project called Engine and doesn't actually depend on Qt. Instead I decided to use Boost so that if I one day decide to ditch Qt I won't have to throw out my model.
In designing the model I had two principal design concerns: Everything has to implement interfaces and everything has to be interchangeable. By defining everything as an interface (classes with a virtual destructor and all pure virtual methods returning types easily resolved without any additional code) this allows for additional expansion later at the hands of myself or somebody else who decides on a better implementation of any component of the engine. It also allows the Qt plugin system to be able to handle references to any of the types in the engine so that a future plugin interface can use them.
The structure I decided on is similar, but not identical to, the one that I used in the python version of this. The basic unit of a simulation is the Block which implements the IBlock interfaces. There are three child interfaces of IBlock: IEntryBlock, IExitBlock, and IModelBlock. IBlocks define inputs (IBlockInput) and outputs (IBlockOutput) and are contained in Models (IModel interface) which represents units to be simulated. Blocks may define entries and exits (IEntryBlock & IExitBlock) and hold any number of interconnected blocks. Entries with the same name as an Exit will be set to the value of the Exit at the end of each step. This allows for persistent variables in the structure (as opposed to just putting them in code). IBlockOutputs can be attached to multiple IBlockInputs but each IBlockInput may only be attached to one IBlockOutput. Boost Signals2 are used for maintaining the cleanliness of the model so that when an input has its attached output replaced, the formerly attached output is notified. IBlocks also signal when the have an input or output added or removed. Models can also be used as blocks (IModelBlock) inside of other Models, allowing for nested execution and creation of reusable components. When a simulation is running, a block can be re-used multiple times in different "contexts". An example of this is a model that is used inside an IModelBlock. The model and all of its component blocks are only instantiated once, but the IModel could be used in multiple IModelBlocks which could be placed inside of different parent IModels. For this reason, I created the IContext interface. An IBlock is passed the IContext whenever an option is set, the block is initialized, or the block is executed. The block is expected to store all of its input, output, and stored values inside the context and not actually use any class member variables to store values between steps. In the case of an IModelBlock, it is expected that the block will "own" a child context in a parent context. This is so that each "instance" of the model being executed as a child model will have its own context to store variables in. The root context and root model are tracked by an IEngine. The IEngine takes care of setting the number of steps that constitute a simulation, the size of the delta value between steps, and what step the simulation is on. The IContext does the actual execution and is created by the IEngine (IEngine::getContext()). It is expected that when executing a step the IContext will queue up all the IBlocks in the IModel that have no inputs and that once all the inputs are set for an IBlock it will be queued for execution. A step is finished when there are no longer blocks to execute. To facilitate looping blocks like those that are found in Simulink, the IContext should re-queue a block for execution each time an input is set again after all of its inputs have already been set. While this leads to the possibility of infinite loops, the connection function should try to locate possible loops and stop them from happening.
As for exact implementation, I have created an IEngine called SimpleEngine (creating DefaultContexts) and I plan on creating a ThreadedEngine (creating ThreadeContexts) since the execution of blocks can become parallel at points where an IBlockOutput connects to multiple IBlocks. Other that those specifics, the default implementation is pretty much as specified above. The IModelBlock implementation subscribes to the signals on the IModel which are fired when IEntryBlocks and IExitBlocks are added/removed so that it can keep its IOs in sync.
A gist of how a simple simulation can be set up and run:
[gist id="4391549" file="simulate_basic_test.cpp"]
The next thing on the docket before I start off on creating all of the system blocks (basically, trying to clone the functionality of the blocks listed for Simulink) and making the GUI is to create a set of interfaces for describing the model in files. This is so that I can create an interface to a generic loader allowing for multiple file formats to be loaded (XML, JSON, BSON, something binary, etc) for describing simulations.