From 42b812e3a7649f68e26083026eec4771d443bb63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Cambi=C3=A9?= Date: Fri, 5 Jul 2019 22:27:41 +0100 Subject: [PATCH] Allows multiple hub handling in MultiPumpController 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. --- pycont/__init__.py | 1 + pycont/_version.py | 2 +- pycont/controller.py | 107 ++++++++++++++++---------------- tests/pump_multihub_config.json | 44 +++++++++++++ tests/pycont_test_multihub.py | 26 ++++++++ 5 files changed, 127 insertions(+), 53 deletions(-) create mode 100644 tests/pump_multihub_config.json create mode 100644 tests/pycont_test_multihub.py diff --git a/pycont/__init__.py b/pycont/__init__.py index 6d5cd30..a0bfb15 100644 --- a/pycont/__init__.py +++ b/pycont/__init__.py @@ -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()) diff --git a/pycont/_version.py b/pycont/_version.py index 1b1a934..1f356cc 100644 --- a/pycont/_version.py +++ b/pycont/_version.py @@ -1 +1 @@ -__version__ = '0.9.9' +__version__ = '1.0.0' diff --git a/pycont/controller.py b/pycont/controller.py index 5b63d14..61bdf00 100644 --- a/pycont/controller.py +++ b/pycont/controller.py @@ -1224,7 +1224,7 @@ 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. @@ -1232,23 +1232,27 @@ class MultiPumpController(object): """ 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 @@ -1268,23 +1272,26 @@ 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): """ @@ -1292,8 +1299,8 @@ def set_pumps_as_attributes(self): """ 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) @@ -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): @@ -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): @@ -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) @@ -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. @@ -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 @@ -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 @@ -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. @@ -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. @@ -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. @@ -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) diff --git a/tests/pump_multihub_config.json b/tests/pump_multihub_config.json new file mode 100644 index 0000000..ff0df51 --- /dev/null +++ b/tests/pump_multihub_config.json @@ -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" + } + }}] +} diff --git a/tests/pycont_test_multihub.py b/tests/pycont_test_multihub.py new file mode 100644 index 0000000..7be4bb2 --- /dev/null +++ b/tests/pycont_test_multihub.py @@ -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")