One of the machines our software controls builds oligonucleotides by pushing various chemicals through a fine glass mesh. To do that, the machine dispenses the chemical into a common line and then pumps it to the destination. Logically, the machine looks like this:
|--------| |----------|
+-------+----+------| PUMP |--+-----+----| Column |----+
| | | |--------| | | |----------| |
Neutral A B | | |
| | |
+-----+-----+-----+ +--------------------+---- Waste
| | |
Argon S O
Flow is from left to right. The chemicals at the top are delivered by turning the pump. If we want to push 100 microliters (100 µl) of A through the column, we first open the Neutral valve and run the pump until the entire line through the column is filled with the Neutral solution. Then, the software closes the Neutral valve and switches the output to target waste. Then the A valve is opened and the pump is turned long enough to inject 100 µl into the line. The output valve is switched to target the column, valve A is closed, Neutral is opened, and the pump turns to push the chemical through the column.
It’s a little more complicated than what I describe, of course, but that’s the gist of it. Pump delivery is relatively easy because we know the length (more importantly, the volumes) of each tube segment, and we know very precisely how much liquid is moved with each revolution of the pump. If we want 100 µl and the pump moves 20 µl per revolution, then we run the pump for five revolutions. The pump is operated by a stepper motor, so we can vary the pump speed by controlling the rate at which we issue step commands to the motor. All we have to do is tell the pump controller, “do X revolutions in Y seconds.” The controller takes care of it and reports back when it’s finished. If we want to deliver 20 µl in three minutes, we tell the pump to do one revolution in 3,000 milliseconds. The motor controller figures out how many microsteps that is, and sends the motor pulses as required.
For reasons I won’t go into, some chemicals are better delivered by pushing with an inert gas (Argon, in this case). Each of the substances on the bottom is sourced from a bottle that’s under positive pressure. If the S valve is opened, liquid starts flowing, and continues to flow until the valve is closed. We know that at our standard operating pressure, the liquid will flow at a rate of about one microliter every seven milliseconds. So if we want to inject 20 µl of S, we open the S valve for 140 milliseconds. We can then close the S valve and open the Argon valve long enough to push the chemical through the column.
The problem comes when we want to vary the speed of Argon delivery. With the pump, all we have to do is slow the pump speed. But we can’t vary the bottle pressure so we have to toggle the Argon valve to simulate pumping. And that’s where things get kind of tricky.
Imagine, for example, that you want to deliver 20 µl of S smoothly over three seconds, rather than at the standard rate of 140 ms. If we could switch the valve on and off quickly enough, that wouldn’t be too difficult. We already know that we can pump 1/7 µl in a millisecond, so if we could toggle the valve every millisecond we could have 140 pulses of 1 ms each, with a 20 ms wait between each pulse. That would deliver in 2,940 ms.
Unfortunately, those valves don’t turn on and off instantly. It takes about 25 milliseconds for a valve to open after it’s energized. And it takes another 25 milliseconds for the valve to close completely after we cut power to it. So we can’t switch the valve faster than once every 50 milliseconds. And during at least part of that time the valve is partially open. So some liquid flows before the 25 ms is up. We can measure that, of course, and we could calibrate each valve if we wanted to. In practice, that calibration isn’t all that helpful. We can do almost as well by taking an average of all the valves in the system.
If you’re wondering how long 50 milliseconds is, a typical eye blink is between 300 and 400 milliseconds. So, whereas you can blink your eyes maybe three times per second, I can toggle these valves on and off 20 times per second.
The other problem is that the computer controlling the valve doesn’t have infinite resolution. Wait precision is typically about five milliseconds. If I want a seven millisecond wait, I’ll typically get something between 7 and 12 ms.
All those uncertainties mean that if I want to open the valve for what I think is 7 ms to dispense 1 µl, it takes 57 ms and I could end up with 2 µl flowing from the valve. This is what we end up calibrating. The tube volume from the farthest Argon-propelled valve to the waste is, say, 4,000 µl. We’ll clear the line (blow it out with Argon), and then start toggling the S valve in one-µl pulses until liquid flows from the waste. If it takes 2,500 pulses, then we know that one pulse dispenses an average of 1.6 µl. If it takes 5,000 pulses, we know that one pulse is, on average, 0.8 µl. That’s the number we’ll use in our calculations.
Interfacing with the real world presents a lot of challenges, but it’s incredibly rewarding. I worked on this machine’s software for several months before I got to see one of them in operation; all my work had been against a simulator. It was excited when I finally got to watch one of these machines work in response to user input, knowing that my software had converted the user’s commands into commands that make the machine switch valves and pump liquid through the system. Controlling hardware like that is the most rewarding type of programming I’ve ever done. I did it years ago, and I’m re-discovering just how much fun it can be.