When you develop applications for the lab, probably you want to be able to re-use parts of the code and allow other scientists to build on your work. This problem has been discussed at length around other applications, such as building web services. And in a certain way, developing a website is not completely different from controlling devices in the lab. A user requests data from a specific server (or a device in the lab), which is delivered through a web browser (or a user interface). Many developers of web applications use the Model-View-Controller pattern, also known as MVC. Lab applications can also be built using the same ideas, creating more sustainable code.
Generally speaking, the MVC pattern is a set of rules on how to structure your code in order to make it reusable and easy to maintain. The MVC framework, although originally developed for desktop computing, has become popular in other fields, for example for designing web applications. Web and desktop applications have specific definitions for the model, the view, and the controller. Lab applications, therefore, have to update those definitions in order to consider the different elements that make up an experiment.
An experiment will have devices generating data on one side and the user on the other. The software will sit in between, it will communicate to the devices the user commands, and it will output back data for the user to interpret. When we have to develop software for an experiment, we are going to start by building drivers for the devices. We are going to define the steps that make up an experiment, we are going to allow the user to change some parameters, and we will show the data back to the user. If we lay out our code in a smart way, we will be able to utilize the same code in different projects and, more importantly, exchanging devices or changing the steps of an experiment will be simple. Let's see what is the role of each of the elements in the MVC pattern for the lab.
The Controller is nothing more than the drivers that we are going to use for communicating with the devices. Sometimes we will develop our own drivers (see our post on How to Write a Driver with Lantz), sometimes we will use libraries such as PyDAQmx (see our post Controlling a National Instruments Card with Python). Therefore, in our projects we may or may not find a folder called Controller, it will depend on which driver we use and how it was packaged. The driver can also be collected from other repositories as long as it is a standalone Python file. Controllers reflect exactly what a device does, what inputs it takes, how it communicates with the computer, etc.
The Model is, perhaps, the most interesting part of the code because it defines the way we, the users, interact with the devices and the logic of the experiment itself. Imagine that you want to acquire a time trace of an analog signal with a DAQ card. The first card you buy is a cheap one that allows you to acquire point by point in time. After you get a driver for the card, you build a Model using the driver and you create a loop to acquire as many data points as you wish. We call that method acquire_timetrace and it takes two arguments, the number of points and the delay between them. Normally, each model will be linked to a specific controller.
The View is, technically, the place where you can locate everything related to how you show data to the user, and how the user can change the parameters of the experiment. In practice, it is the collection of files that build up a Graphical User Interface (GUI). Within the GUI you will set, for example, the length and delay of the time trace you want to acquire and in turn, you will use the model to acquire the data. You can plot the results back to the user and save them to disk, etc. It is important to note that, in this case, the user interacts through the view with the model and never directly with the controller of a device.
At this point, you may be wondering what is the advantage of splitting the development into the three components. Imagine you decide to buy a new card that is able to acquire a time trace of a signal, and the driver for the card is already available. You can just adapt the model in order to use the new controller but the view will not change at all. Provided that the methods defined in the model are called the same and take the same inputs, there is no need to alter the code downstream.
Moreover, since you didn't alter the code for the controller, you can easily share it with people that have the same device, but perhaps want to perform a different experiment. This detail is important because if you include the logic of your own experiment into your drivers, you will need to alter the controller beyond the device's inherent capacities every time you change what you want to do. Moreover, people with different needs will also be forced to alter the driver, and therefore it will be almost impossible to collaborate. Always keep in mind that controllers should reflect exactly what devices do; in our example, the cheap card was not prepared for time traces, therefore the controller shouldn't have such option. The model, however, can easily implement time traces through a loop.
We can go one step further with the models. Imagine you want to monitor a signal while you change a voltage. The cheap card can't do it automatically, so you add in your model a new method called scan which takes the inputs you think are appropriate. But that is not all you want to do. For example, before you start the scan, you want to open a shutter by setting a digital output to high and close it afterward. You could implement everything directly on your device model, but it is a much better idea to add a new model for the experiment. In this new model, you can place all the logic of the measurement you are performing; you can decide in which order things happen, what values have to be set, etc. Moreover, you could use different devices; perhaps the shutter is not connected to the same card that acquires the signal.
Adding a new layer is a great way of keeping your code organized. If you use an Experiment model, you can pass exclusively that model to your view. When you deal with only one device the advantage is not obvious, but as soon as you start adding more and more devices it becomes very useful. You can add the appropriate saving methods directly to the experiment class, and therefore you know you will be consistent in your way of saving data. You will always know which headers to use and how to retrieve the data later on. Moreover, as soon as you have an Experiment model, you can easily perform measurements both through the command line or through user interfaces.
The MVC pattern becomes very useful when laying out projects, at Uetke we normally start by creating the three folders and then we add the needed elements to each one. Of course, not every project is going to be equal and sometimes the needs are going to be different. However, if you find yourself developing software for controlling a setup, having a clear layout since the beginning will save you a lot of time while the complexity increases. Moreover, if you document it correctly you allow others to quickly find the places of your code that need to be updated.
If you want to learn everything about how to use the MVC design pattern for laboratory software, check our Python For The Lab Course, where we cover these topics and many more. If you want to see how the MVC pattern looks like in a real-world example, you can check our development for controlling an optical tweezer, also available on Github.
Header photo by Alexandru Acea on Unsplash