Using F# for scientific instrument control

This post is part of the F# advent calendar 2015 which is packed with some outstanding reading material :-).

Over the past few years, I have worked in a research lab at the University of Warwick where we often develop custom instrumentation for our experiments. Along the way, I’ve found that good experiment control software presents many interesting challenges: it requires a combination of concurrent control of several external devices and real-time data charting. Cancellation support is essential because experiments can be long-running and you may want to stop them part way through without losing your data. Good error handling and logging are needed to find the causes of failure when it occurs. Sometimes it’s necessary to implement computationally demanding signal processing in software as well. Fortunately, I discovered F#, which is the perfect Swiss army knife for many of these problems and I’d like to tell you a bit about how we use it!

I was faced with a problem, because not everyone has access to a lab full of pricey equipment to play with… so for demonstration purposes, we’re going to subvert the sound card instead ;-).

A basic function generator

First, we want to generate some sounds… For that, we’ll make a simple function generator. We’d like to be able to play sine waves and frequency sweeps (where the frequency is smoothly ramped from an initial frequency to a final one over some duration). We’d like to be able to tell our function generator to play and stop, and finally, we’d like to be able to grab the next n samples. In general, communication with lab instruments is based on messages, so MailboxProcessor agents are the perfect abstraction. They allows us to define the set of possible messages we can send to our virtual instrument and define a processing workflow which will handle the messages in order of arrival, updating the state of the agent accordingly. Without further ado, here’s a simple model for our function generator (amplitudes are in float32 for our audio I/O).

Our agent is going to be in one of two states: either waiting for playback to be started, or playing the waveform. StartPlayback and StopPlayback commands cause it to switch between the two states. Whenever samples are requested via ReadSamples, the agent samples values of the relevant mathematical function at discrete intervals. These samples are returned asynchronously via the supplied reply channel. It also needs to keep track of how many samples it has read out already so that it can continue from same position next time. If a new waveform is set during playback, the count is reset to zero.

We are using an audio I/O framework called NAudio which defines the types WaveOut and IWaveProvider. WaveOut can be initialised with an instance of IWaveProvider where after it request samples from it and writes them to an output buffer. IWaveProvider is implemented by sending a ReadSamples message to the agent and awaiting the reply. I’ve omitted these details here to keep things simple, but the full code is available on GitHub.

Ok, so now we have some sounds — let’s measure them!

Detecting waves

In physics, it’s often possible to set up an experiment so that the interesting signal oscillates at a known frequency (but its amplitude or phase changes during the measurement). In the 40’s, a physicist in Princeton came up with a device which is only sensitive to signals at a specific frequency, known as a lock-in amplifier. The upshot of this is that your experiment also becomes only sensitive to noise near that frequency, greatly improving signal-to-noise. This was a boon, enabling many new experiments.

So let’s build one!

Sine waves have a very special property: if you multiply two of them together, then the resulting function’s average value (from -∞ to +∞) is zero unless they have exactly the same frequency. This property underpins Fourier analysis, lock-in amplifiers, modems and indeed any form of radio communication. This is with the help of an electronics component called a mixer which essentially multiplies two signals together.

We are going to follow the same pattern of using a MailboxProcessor agent, only this time, we are going to have an IObservable output containing the signal. The two most important settings for a lock-in amplifier are the reference frequency and the time constant. You can think of the time constant as a moving window, taking the average of the signal after the input is mixed with the reference sine wave. A longer time constant improves signal-to-noise but also makes the device slower to respond to changes in input. In practice, this is implemented using a low-pass filter which filters out high frequencies. After this stage, it is safe to decimate our signal (only take every n-th sample and ignore the rest) without risk of aliasing because we have taken out the high frequency components. Of course, we should be able to change the settings of our lock-in, as well as start and stop recording. Here’s our model.

The settings are mutable fields in a record which will be accessed from multiple threads, and therefore require a synchronisation lock. The agent is quite trivial to implement.

(Update: Corrected a minor bug due to stereo vs. mono sampling kindly pointed out by Mark Heath, the creator of NAudio.)

The tricky part comes in implementing the signal processing. To me, this is a place where F# truly shines. First, we will build a small library of digital signal processing functions for IObservable. This looks like so:

There are a couple of things to note here. Firstly, we will need to branch the signal into two paths and process them differently, before recombining them by computing their vector magnitude. The intermediate signals are known as in-phase and quadrature respectively. In the interest of getting to the next section, I won’t go into further detail here but if you’d like to know more, drop me a line on Twitter. Secondly, the parameters to the lowPass and mix functions are, themselves, functions because they will need to grab the current time constant and reference frequency as the samples arrive. Now our signal processing pipeline for the lock-in amplifier looks like so:

And now for some experiments

Finally, we are ready to run a couple of simple experiments. We’ll first play a sine wave at the lock-in reference frequency and step through series of amplitudes to see how the lock-in response changes.

Looks pretty convincing to me! The rise time of the signal at each step is related to the lock-in time constant: the longer the time constant, the smoother the steps.

Next, we will play a frequency sweep at a fixed amplitude around the lock-in reference frequency and see how the response changes.

As expected, we only get a large response at the centre of the frequency sweep. Nice! :-)

The big picture

With relatively little fuss, we’ve managed to demonstrate some experimental physics techniques. The resulting code is quite straightforward and expressive. I think F# has a huge amount of potential in experiment control, thanks to its powerful features for handling concurrency and signal processing. I also think that it’s a real sweet spot for the language because it leverages some of its most unique features. And in a field where some are still programming like this, change is overdue ;-).

The main barrier to adoption, I think, is the lack of existing libraries for common lab equipment. We have actually already built several and would love to start sharing these with other people. Unfortunately, as experimental results are our main priority, we haven’t yet found the time to get manage our code as an open source project and add good documentation. That said if you are interested in working on it with us, then please do get in touch.

Merry Christmas and happy coding! :-)