A generic pump device for Micro-Manager (work in progress)

Hello everyone,

Going back to the BDPathway 855 and other high content microscopy systems, I see the need for a generic pump device in Micro-Manager, which can handle “pump like” instruments (that I know of):

There have been efforts to add fluidics to Micro-Manager via plugins (check out the excellent NanoJ-Fluidics from the Henriques lab) but as far as I understand, fluidics devices in these plugins are assembled around direct FreeSerial calls. A much better solution for this would be to write MM device adapters (derived from an appropriate class) that can then be accessed through Java / Beanshell calls to MMCore. These adapters would then log errors and messages to the central corelog, and their initialisation and shutdown methods would be called at appropriate times.

I am not a fluidics expert, so if I’ve missed anything, don’t hesitate to let me know!

What about reusing existing devices?

If you check through the Device Adapters, you will find Mark’s HamiltonMVP adapter for Hamilton Valves. Valves are perfectly handled by a StateDevice + some properties (direction of rotation for example).

A continuous pump would be the same: (on/off states, direction and flow rate as properties). This falls over however if the peristaltic pump is also used to dispense set volumes. Or if you have a syringe pump, you may need to track the volume at all times to know how much liquid can still be aspirated or dispensed.

The (generic) pump device:

Here is a list of methods I think we need to interact with a pump, which aren’t available in other MM devices:

  • Methods for aspirating / dispensing liquids (continuously or as a set volume)
  • Method for homing the steppers in a syringe pump and possibly a method for priming the pump?
  • Calibration method (microlitres <-> steps conversion)
  • Setting the flow rate (microlitres / min)

These should cover continuous pumps as well as syringe pumps. For pipettors (I am not considering those quite yet), maybe later add methods for loading and ejecting tips.

For now, these are the methods I’ve defined:

int PumpInstance::EnablePumpVolume(bool state) { return GetImpl()->EnablePumpVolume(state); }
int PumpInstance::GetPumpVolumeEnabled(bool& state) { return GetImpl()->GetPumpVolumeEnabled(state); }
int PumpInstance::SetFlowRateUlPerMinute(double fr) { return GetImpl()->SetFlowRateUlPerMinute(fr); }
int PumpInstance::GetFlowRateUlPerMinute(double& fr) { return GetImpl()->SetFlowRateUlPerMinute(fr); }
int PumpInstance::SetVolumeUl(double volUl) { return GetImpl()->SetVolumeUl(volUl); }
int PumpInstance::GetVolumeUl(double& volUl) { return GetImpl()->GetVolumeUl(volUl); }
int PumpInstance::SetMaxVolumeUl(double volUl) { return GetImpl()->SetMaxVolumeUl(volUl); }
int PumpInstance::GetMaxVolumeUl(double& volUl) { return GetImpl()->GetMaxVolumeUl(volUl); }
int PumpInstance::SetStepsPerUl(double sUl) { return GetImpl()->SetStepsPerUl(sUl); }
int PumpInstance::GetStepsPerUl(double& sUl) { return GetImpl()->GetStepsPerUl(sUl); }
int PumpInstance::Aspirate() { return GetImpl()->Aspirate(); }
int PumpInstance::Aspirate(double volUl) { return GetImpl()->Aspirate(volUl); }
int PumpInstance::Dispense() { return GetImpl()->Dispense(); }
int PumpInstance::Dispense(double volUl) { return GetImpl()->Dispense(volUl); }
int PumpInstance::Home() { return GetImpl()->Home(); }
int PumpInstance::Stop() { return GetImpl()->Stop(); }

Based on these, I have added a demo pump device to the DCAM hub:
image

… and all its parameters and methods are accessible from beanshell:

For now, I’ve made these changes to MM 1.4.23 (in Linux) as I feel a bit more confident with the 1.4.x codebase, sorry! :slight_smile:

There’s quite a bit of code I wrote to get to this point, I’ll try to clean it up and send it to Mark and Nico.

My next step will be to modify the HamiltonMVP Hub device to include PS/D syringe pumps into the daisy chain. These will then be accessible via Java or Beanshell.

Cheers,
Egor

Hi Egor,

I’m working on making firmware/device adapters/plugins for some homemade pump devices at the moment, with a couple of (very basic) ones now mostly working - if there’s an opportunity to use some standard serial/other calls, I’d be happy to change things to match that. Hopefully I’ll be able to put these out there before too long

Regards,
Sunil

Hi Sunil,

I don’t think you need to change anything in your serial protocol. If it works as is, that’s awesome! Just make sure it’s well documented :slight_smile:

For the functions you might want to implement in your pump driver / brain, it really depends on what type of pump you are putting together. The most basic driver would understand dispense and stop, then possibly a way to choose the direction (aspirate, dispense, stop), then flowrate and aspirate / dispense set volumes.

If the code makes sense, I hope that developers will take inspiration from other existing device adapters, and all similarly specced pump devices will be interchangeable (just like cameras, shutters, LED light sources… already are).

Or that’s the plan! Good luck with your pumps, and I’ll keep you in the loop if this gets implemented in Micro-Manager and if/when I have a real device adapter to share (hopefully the Hamilton PSD/2 syringe pump if I didn’t break the one I was using for testing).

Kind regards,
Egor

Hi @EP.Zindy,

I find it difficult to judge the usefulness/completeness/simplicity of the api you propose because I have no experience whatsoever with pump devices. Are there other (hopefully open) apis out there that we can look at? Does someone already have a good pump interface that we can copy?

Cool!

I second what Nico says: if I were designing this, I would want to find as many real devices out there as possible, so that the API reasonably covers their functionality without individual device adapters having to repeat similar logic too much. (This is admittedly a higher standard than we had for the existing device types – trying to learn from history.)

But I think the basic idea is quite good. Here are a few things that quickly came to mind from an API design perspective:

  • I think having both “steps” and “microns” in our stage interfaces has resulted in a lot of messy or questionable code. I think it is worth considering adding only “steps” on the MMDevice interface and performing the conversion to/from microliters in MMCore.
  • Having separate named methods for Aspirate and Dispense might result in user code with if statements to branch on positive vs negative volume. It is also a little confusing in the case of a peristaltic pump because the same direction will aspirate and dispense, depending on which end of the tube you are looking at. I think it will be cleaner to use a single method that allows negative volumes (and also allow negative flow rates to be set). Hopefully most peristaltic pumps have an inherent “forward” direction (to be mapped to positive volumes and flow rates); for those that don’t we can define it as clockwise when looking at the pump from the front. For syringe pumps we should almost certainly define dispensing as positive.

Hi everyone,

some progress on this! I really felt I needed to finish writing a real device adapter before I could address some of the concerns. So, I present to you, my HamiltonMVP device adapter (COVID edition) which now also recognises syringe pumps and Microlab devices comprising multiple valves and syringes in a single device.

Devices themselves are all determined by the first few characters of the firmware and from there, each Valve and Syringe pump added to the hub accordingly is given its own peripheral. If multiple valves / syringes are present at a device address, the Hub knows to differentiate them. For example, my PSD/2 which comprises both a syringe pump and a 4 position valve would be HamiltonPSD-a and HamiltonMVP-a, whereas a Microlab 600 (two syringes, two valves) would be HamiltonPSD-a1, HamiltonPSD-a2, HamiltonMVP-a1, HamiltonMVP-a2 (untested).

My Hamilton test setup

The hardware daisy chain I put together looks something like this:


Left to right: PC connection either through my trusted Y-Pipe or a Moxa USB-Serial adapter, the PSD/2 syringe pump and the Hamilton MVP valve setup as a 6 position valve. (The Toshiba PSU I had on hand only delivers 19V, so I used a boost converter to get the 24V needed by my gear. So far so good :thinking:).

Both Hamilton devices are daisy chained on a single serial port (PSD/2 is address a and MVP is address b) and my modified MVP adapter is able to detect both:
image

Then there was the issue of adding as properties all the parameters defining how the Hamilton syringe pump behaves.
image

Then finally, the implementation of the PumpAspirate / PumpDispense / SetFlowRateUlPerMinute methods and some kind of test script to check the validity of the BeanShell syntax.

So here’s a loop through all 6 positions of the MVP valve and either aspirating 50ul at 8000ul/min (even port) or dispensing 50ul at 4000ul/min (odd port).

gui.clearMessageWindow();

pumpLabel = "HamiltonPSD-a";
valveLabelA = "HamiltonMVP-a";
valveLabelB = "HamiltonMVP-b";

isAspirate = true;
// Switch between all 6 positions on the MVP valve
positions = mmc.getStateLabels(valveLabelB);
for (int i=0; i<mmc.getNumberOfStates(valveLabelB); i++)
{
	print("Pos "+i+":"+positions.get(i));
	//Set the main valve
   mmc.setState(valveLabelB,i);
   
   if (isAspirate)
   {
      //Syringe valve input
      mmc.setState(valveLabelA,0);
      //Non-blocking, so wait for both valves to stop
      mmc.waitForDevice(valveLabelA); mmc.waitForDevice(valveLabelB);
      //Set an aspiration flow rate and aspirate 50ul
      mmc.setFlowRateUlPerMinute(pumpLabel,8000);
      mmc.PumpAspirate(pumpLabel,50);
   } else {
      //Syringe valve output
      mmc.setState(valveLabelA,3);
      //Non-blocking, so wait for both valves to stop
      mmc.waitForDevice(valveLabelA); mmc.waitForDevice(valveLabelB);
      //Set an aspiration flow rate and dispense 50ul
      mmc.setFlowRateUlPerMinute(pumpLabel,4000);
      mmc.PumpDispense(pumpLabel,50);
   }
   while(mmc.deviceBusy(pumpLabel))
      print("Volume: "+mmc.getPumpVolume(pumpLabel));
   isAspirate = !isAspirate;
}

What have I learned?

I didn’t really need the steps per ul value. I relied on the MaxVolume value, FullStrokeSteps (which itself depends on the user defined low/high resolution value) and FlowRate value. But, this may be different for peristaltic pumps (I am thinking of doing a Cole Parmer Masterflex next).

A lot of these operations are non-blocking. I was really conflicted about making them blocking or not. In the example above, I can operate multiple valves at the same time (if appropriate) and wait for them to stop before doing my aspirate / dispense operation. I can also query the Volume while the syringe is operating and halt a device if needed. But! That means adding checks (waitForDevice) which may timeout if CoreTimeout isn’t large enough. Or use while(mmc.deviceBusy(pumpLabel)); which isn’t ideal in case something does goes wrong with the device and the loop never exits.

Onto Mark’s and Nico’s comments:

Are there other (hopefully open) apis out there that we can look at? Does someone already have a good pump interface that we can copy?

In terms of device adapters, we have two already! The Ismatec MCP peristaltic pump (code) and the WPI Alladin syringe pump (code). It would be interesting to convert them to the new pump device model, even if we would need to call them something else to preserve compatibility with existing code and configs.

I was going to look at NanoJ-Fluidics next, but I’m open to any suggestions. I “feel” I have enough methods to start doing things with the syringe pump and build the stuff I may need later from them (for example a n times mix would be n x (aspirate/dispense)) I’ll come back to you when I have a peristaltic pump accessible via beanshell.

if I were designing this, I would want to find as many real devices out there as possible, so that the API reasonably covers their functionality without individual device adapters having to repeat similar logic too much.

I agree about trying not to repeat too much code in the adapters, so I’ll come back to you. I think there will be differences in the way volumes are calculated depending on the type of pump:

  • For a syringe pump, volumes depend on flow rate / syringe volume / full stroke steps.
  • For a peristaltic pump, volumes depend on speed and direction of the motor / tube diameter / ul per steps and number of steps or for how long the pump is on if it has a DC motor rather than a stepper.

There will be differences that for now are quite difficult to judge. I am hoping for now that I can get away with setFlowRateUlPerMinute / aspirate / dispense as Core methods and everything else as properties.

(This is admittedly a higher standard than we had for the existing device types – trying to learn from history.)

No worries, this is why we’re having this discussion and I will modify my code accordingly before dropping a huge pull request on you! (MMCore, Pump Device, DemoCamera, HamiltonMVP … I will split those up obviously).

I think having both “steps” and “microns” in our stage interfaces has resulted in a lot of messy or questionable code. I think it is worth considering adding only “steps” on the MMDevice interface and performing the conversion to/from microliters in MMCore.

I see what you mean but am not exactly sure how to deal with pumps that don’t have steps like the pumps in this VK-01 Bartender robot. If they are cheap, you just know these pumps will find their way into a DIY system!

One thing I wasn’t sure about is the range of flow rates we really need. microlitre/min for me, but what if someone needs many orders of magnitude more or fewer than this? 5l/min need to be coded as 5000000ul/min … Acceptable for now?

Having separate named methods for Aspirate and Dispense might result in user code with if statements to branch on positive vs negative volume.

We probably have different use case in mind! I have never dealt with negative volumes on my Biomek FX or on the high content systems that could also dispense volumes with a pipettor. The volumes were always either what you had in a well or what you had in the tip and were always positive or null. I really don’t like the idea that if you have some sort of rounding error (say in a serial dilution) you could end-up aspirating liquid instead of dispensing it. Having said that:

It is also a little confusing in the case of a peristaltic pump because the same direction will aspirate and dispense, depending on which end of the tube you are looking at.

Yes, like on the XYstage, we need a direction flag to control the direction on a peristaltic pump and inverse it if needed by a particular setup.

I think it will be cleaner to use a single method that allows negative volumes (and also allow negative flow rates to be set). Hopefully most peristaltic pumps have an inherent “forward” direction (to be mapped to positive volumes and flow rates); for those that don’t we can define it as clockwise when looking at the pump from the front. For syringe pumps we should almost certainly define dispensing as positive.

Let me think about it and check the serial protocol on a few peristaltic pumps. I understand what you’re saying, but I’m not sure if allowing negative volumes really is going to simplify programming vs having aspirate and dispense and (for peristaltic pumps) a direction inversion property. I’ll come back to you on that one :slight_smile:

Edit I thought I may as well add pipettor commands now rather than later. I think the expressions “Load a tip” and “Eject a tip” are relatively standard, so that’s what I used.

I removed StepsPerUl which doesn’t make much sense other than as a property. For instance, I didn’t use it for the Hamilton PSD (relying instead on MaxVolume and Full stroke steps).

What are left are the following methods for the Pump class:

// These define parameters:
   int EnablePumpVolume(bool state);
   int GetPumpVolumeEnabled(bool& state);
   int InvertPumpDirection(bool state);
   int GetPumpDirectionInverted(bool& state);
   int SetFlowRateUlPerMinute(double fr);
   int GetFlowRateUlPerMinute(double& fr);
   int SetVolumeUl(double volUl);
   int GetVolumeUl(double& volUl);
   int SetMaxVolumeUl(double volUl);
   int GetMaxVolumeUl(double& volUl);

// These define actions:
   int Aspirate();
   int Aspirate(double volUl);
   int Dispense();
   int Dispense(double volUl);
   int Home();
   int Stop();
   int LoadTip(); //Pipettor command
   int EjectTip(); //Pipettor command

I removed SetStepsPerUl and GetStepsPerUl as their role is either as temporary variable or can be filled by a property if needed but isn’t generic enough to warrant a method in MMCore.

I made this decision after I looked at the Cole Parmer Masterflex and the parameters for a peristaltic pump are completely different. Here, the flow rate is defined by the tube diameter and the speed (RPMs) of the shaft. Then the volume dispensed is a function of flow rate and number of turns of the shaft.

I see the SetVolumeUl (set current volume) as a bit of a hack, as I personally wouldn’t have a need for it, but it’s “not implemented” by default.

I added the InvertPumpDirection and GetPumpDirectionInverted methods after Mark’s comment. if a (symmetric) pump is set a certain way and Aspirate and Dispense are inverted in Micro-Manager, this will correct for it.

So what I’m asking now is, are we happy with the method names and do they make sense? Are there too many? too few? And remember, this is for the “generic set” if you can call it that way. Then anything else that needs adding can be done via property variables.

One last thing, no “Prime” function, which brings liquid all the way to the tip of the syringe needle, tube, etc… as this can be done with a dispense operation which is manually interrupted when the bubbles are gone and the liquid is just right. But! All the parts are available for implementing such a function.

Edit The MMCore member functions syntax for the pump device is another thing I wanted to discuss. Do they read OK as is? Or could we simplify the ones that leave no doubt that they are pump commands (e.g. mmc.PumpLoadTip(pumpName) to become mmc.loadTip(pumpName) )?

For now, these are the member function names I used:

   void enablePumpVolume(const char* deviceLabel, bool enable) throw (CMMError);
   bool isPumpVolumeEnabled(const char* deviceLabel) throw (CMMError);
   void invertPumpDirection(const char* deviceLabel, bool invert) throw (CMMError);
   bool isPumpDirectionInverted(const char* deviceLabel) throw (CMMError);
   void setFlowRateUlPerMinute(const char* deviceLabel, double fr) throw (CMMError);
   double getFlowRateUlPerMinute(const char* deviceLabel) throw (CMMError);
   void setPumpVolume(const char* deviceLabel, double volume) throw (CMMError);
   double getPumpVolume(const char* deviceLabel) throw (CMMError);
   void setPumpMaxVolume(const char* deviceLabel, double volume) throw (CMMError);
   double getPumpMaxVolume(const char* deviceLabel) throw (CMMError);
   void PumpAspirate(const char* deviceLabel) throw (CMMError);
   void PumpAspirate(const char* deviceLabel, double volume) throw (CMMError);
   void PumpDispense(const char* deviceLabel) throw (CMMError);
   void PumpDispense(const char* deviceLabel, double volume) throw (CMMError);
   void PumpHome(const char* deviceLabel) throw (CMMError);
   void PumpStop(const char* deviceLabel) throw (CMMError);
   void PumpLoadTip(const char* deviceLabel) throw (CMMError);
   void PumpEjectTip(const char* deviceLabel) throw (CMMError);