Skip to content

Commit

Permalink
Allows multiple hub handling in MultiPumpController
Browse files Browse the repository at this point in the history
This new implementations allows to control pumps shared between
different controllers (i.e. different serial connections) effectively
allowing to control more than 15 pumps with the same object.

Rather than introducing a new, higher level of abstraction, the existing
MultiPumpController has been modified to allow both the former config
syntax (with the JSON settings assuming a one-to-one relationship
between serial connection and MultiPumpController) and the updated
syntax featuring an "hubs" parameter aggregating the pump settings per
serial connection, providing the I/O settings of each hub.

Note that, if the "hubs" parameter is provided in the configuration
dictionary, the latter configuration mode is assumed.

A new example has been added to clarify the matter.

Version number has been bumped.
  • Loading branch information
Dario Cambié committed Jul 5, 2019
1 parent dc5d05c commit 42b812e
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 53 deletions.
1 change: 1 addition & 0 deletions pycont/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
from ._version import __version__
from ._logger import __logger_root_name__
from .controller import MultiPumpController, C3000Controller

import logging
logging.getLogger(__logger_root_name__).addHandler(logging.NullHandler())
2 changes: 1 addition & 1 deletion pycont/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.9.9'
__version__ = '1.0.0'
107 changes: 55 additions & 52 deletions pycont/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,31 +1224,35 @@ def terminate(self):

class MultiPumpController(object):
"""
This class deals with controlling multiple pumps at a time.
This class deals with controlling multiple pumps on one or more hubs at a time.
Args:
setup_config (Dict): The configuration of the setup.
"""
def __init__(self, setup_config):
self.logger = create_logger(self.__class__.__name__)

self._io = PumpIO.from_config(setup_config['io'])

if 'default' in setup_config:
self.default_config = setup_config['default']
else:
self.default_config = {}

if 'groups' in setup_config:
self.groups = setup_config['groups']
self.pumps = {}
self._io = []

# Sets groups and default configs if provided in the config dictionary
self.groups = setup_config['groups'] if 'groups' in setup_config else {}
self.default_config = setup_config['default'] if 'default' in setup_config else {}

if "hubs" in setup_config:
for hub_config in setup_config["hubs"]:
# Each hub has its own I/O config. Create a PumpIO object per each hub and reuse it with -1 after append
self._io.append(PumpIO.from_config(hub_config['io']))
for pump_name, pump_config in list(hub_config['pumps'].items()):
full_pump_config = self.default_pump_config(pump_config)
self.pumps[pump_name] = C3000Controller.from_config(self._io[-1], pump_name, full_pump_config)
else:
self.groups = {}
self._io = PumpIO.from_config(setup_config['io'])
for pump_name, pump_config in list(setup_config['pumps'].items()):
full_pump_config = self.default_pump_config(pump_config)
self.pumps[pump_name] = C3000Controller.from_config(self._io, pump_name, full_pump_config)

self.pumps = {}
for pump_name, pump_config in list(setup_config['pumps'].items()):
defaulted_pump_config = self.default_pump_config(pump_config)
self.pumps[pump_name] = C3000Controller.from_config(self._io, pump_name, defaulted_pump_config)
# Adds pumps as attributes
self.set_pumps_as_attributes()

@classmethod
Expand All @@ -1268,32 +1272,35 @@ def from_configfile(cls, setup_configfile):
with open(setup_configfile) as f:
return cls(json.load(f))

def default_pump_config(self, pump_config):
def default_pump_config(self, pump_specific_config):
"""
Creates a default pump configuration.
Args:
pump_config (Dict): Dictionary containing the pump configuration.
pump_specific_config (Dict): Dictionary containing the pump configuration.
Returns:
defaulted_pump_config (Dict): A new default pump configuration mirroring that of pump_config.
combined_pump_config (Dict): A new default pump configuration mirroring that of pump_config.
"""
defaulted_pump_config = dict(self.default_config) # make a copy
# Makes a copy of the default values (this is needed because we are going to merge default with pump settings)
combined_pump_config = dict(self.default_config)

for k, v in list(pump_config.items()):
defaulted_pump_config[k] = v
# Adds pump specific settings
for k, v in list(pump_specific_config.items()):
combined_pump_config[k] = v

return defaulted_pump_config
# Returns the combination of default settings and pump specific settings
return combined_pump_config

def set_pumps_as_attributes(self):
"""
Sets the pumps as attributes.
"""
for pump_name, pump in list(self.pumps.items()):
if hasattr(self, pump_name):
self.logger.warning("Pump named {pump_name} is already a reserved attribute, please change name or do not use"
"this pump in attribute mode, rather use pumps[{pump_name}]".format(pump_name=pump_name))
self.logger.warning(f"Pump named {pump_name} is a reserved attribute, please change name or do not use "
f"this pump in attribute mode, rather use pumps['{pump_name}'']")
else:
setattr(self, pump_name, pump)

Expand All @@ -1310,7 +1317,10 @@ def get_pumps(self, pump_names):
"""
pumps = []
for pump_name in pump_names:
pumps.append(self.pumps[pump_name])
try:
pumps.append(self.pumps[pump_name])
except KeyError:
pass
return pumps

def get_all_pumps(self):
Expand All @@ -1321,7 +1331,6 @@ def get_all_pumps(self):
pumps (List): A list of the all the pump objects in the Controller.
"""

return self.pumps

def get_pumps_in_group(self, group_name):
Expand All @@ -1347,21 +1356,21 @@ def get_pumps_in_group(self, group_name):

def apply_command_to_pumps(self, pump_names, command, *args, **kwargs):
"""
Applies a given command to the pumps.
Applies a given command to the pumps.
Args:
pump_names (List): List containing the pump names.
Args:
pump_names (List): List containing the pump names.
command (str): The command to apply.
command (str): The command to apply.
*args: Variable length argument list.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguements.
**kwargs: Arbitrary keyword arguments.
Returns:
returns (Dict): Dictionary of the functions.
Returns:
returns (Dict): Dictionary of the functions.
"""
"""
returns = {}
for pump_name in pump_names:
func = getattr(self.pumps[pump_name], command)
Expand Down Expand Up @@ -1397,7 +1406,7 @@ def apply_command_to_group(self, group_name, command, *args, **kwargs):
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguements.
**kwargs: Arbitrary keyword arguments.
Returns:
returns (Dict) Dictionary of the functions.
Expand All @@ -1415,7 +1424,7 @@ def are_pumps_initialized(self):
False (bool): The pumps have not been initialised.
"""
for _, pump in list(self.pumps.items()):
for pump in list(self.pumps.values()):
if not pump.is_initialized():
return False
return True
Expand Down Expand Up @@ -1479,7 +1488,7 @@ def are_pumps_idle(self):
False (bool): The pumps are not idle.
"""
for _, pump in list(self.pumps.items()):
for pump in list(self.pumps.values()):
if not pump.is_idle():
return False
return True
Expand All @@ -1500,8 +1509,6 @@ def pump(self, pump_names, volume_in_ml, from_valve=None, speed_in=None, wait=Fa
"""
Pumps the desired volume.
.. note:: Reimplemented as MultiPump so it is really synchronous
Args:
pump_names (List): The name of the pumps.
Expand Down Expand Up @@ -1533,8 +1540,6 @@ def deliver(self, pump_names, volume_in_ml, to_valve=None, speed_out=None, wait=
"""
Delivers the desired volume.
.. note:: Reimplemented as MultiPump so it is really synchronous
Args:
pump_names (List): The name of the pumps.
Expand Down Expand Up @@ -1566,12 +1571,10 @@ def transfer(self, pump_names, volume_in_ml, from_valve, to_valve, speed_in=None
"""
Transfers the desired volume between pumps.
.. note:: Reimplemented as MultiPump so it is really synchronous, needed
Args:
pump_names (List): The name of the pumps.
volume_in_ml (float): The volume to be transfered.
volume_in_ml (float): The volume to be transferred.
from_valve (chr): The valve to transfer from.
Expand All @@ -1584,14 +1587,14 @@ def transfer(self, pump_names, volume_in_ml, from_valve, to_valve, speed_in=None
secure (bool): Ensures that everything is correct, default set to False.
"""
volume_transfered = 1000000 # some big number 1000L is more than any decent person will try
volume_transferred = float('inf') # Temporary value for the first cycle only, see below
for pump in self.get_pumps(pump_names):
candidate_volume = min(volume_in_ml, pump.remaining_volume)
volume_transfered = min(candidate_volume, volume_transfered)
candidate_volume = min(volume_in_ml, pump.remaining_volume) # Smallest target and remaining is candidate
volume_transferred = min(candidate_volume, volume_transferred) # Transferred is global minimum

self.pump(pump_names, volume_transfered, from_valve, speed_in=speed_in, wait=True, secure=secure)
self.deliver(pump_names, volume_transfered, to_valve, speed_out=speed_out, wait=True, secure=secure)
self.pump(pump_names, volume_transferred, from_valve, speed_in=speed_in, wait=True, secure=secure)
self.deliver(pump_names, volume_transferred, to_valve, speed_out=speed_out, wait=True, secure=secure)

remaining_volume_to_transfer = volume_in_ml - volume_transfered
remaining_volume_to_transfer = volume_in_ml - volume_transferred
if remaining_volume_to_transfer > 0:
self.transfer(pump_names, remaining_volume_to_transfer, from_valve, to_valve, speed_in, speed_out)
44 changes: 44 additions & 0 deletions tests/pump_multihub_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"default": {
"volume": 5,
"micro_step_mode": 2,
"top_velocity": 6000
},
"groups": {
"oils": ["oil1", "oil2", "oil3", "oil4"],
"solvents": ["water", "acetone"]
},
"hubs": [{
"io": {
"port": "/dev/trihub",
"baudrate": 38400,
"timeout": 1
},
"pumps": {
"acetone": {
"switch": "0"
},
"water": {
"switch": "1"
},
"oil1": {
"switch": "2"
}
}}, {
"io": {
"port": "/dev/tricable",
"baudrate": 38400,
"timeout": 1
},
"pumps": {
"oil2": {
"switch": "0"
},
"oil3": {
"switch": "1"
},
"oil4": {
"switch": "2"
}
}}]
}
26 changes: 26 additions & 0 deletions tests/pycont_test_multihub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-

import time

import logging
logging.basicConfig(level=logging.DEBUG)

# Import the controller
from pycont import MultiPumpController

# link to your config file
SETUP_CONFIG_FILE = './pump_multihub_config.json'

# and load the config file in a MultiPumpController
controller = MultiPumpController.from_configfile(SETUP_CONFIG_FILE)

# Initialize the pumps in a smart way. Smart way here means that:
#
# - if they are already initialized they are not re-initialized (this would cause their plunger to go back to volume=0)
# - before initializing the plunger, the valve is set to the position specified as 'initialize_valve_position'
# this is defaulted to 'I' and is important as initialization with valve connected to a fluidic path characterized by
# high pressure drop is likely to fail due to the relatively high plunger speeds normally used during initialization
controller.smart_initialize()

controller.apply_command_to_group(group_name="oils", command="transfer", volume_in_ml=5, from_valve='I', to_valve='O')
controller.apply_command_to_group(group_name="oils", command="wait_until_idle")

0 comments on commit 42b812e

Please sign in to comment.