Simulation and Mocking
How to develop new methods with a software mock.
In this guide we will import the MicrolabSTARMock liquid handler class and create a mocked version of the connector. We will check and adjust the configuration of the device, run the initialization procedure, and perform some basic movement.
Simulation and Mocking
There are two kinds of developing methods without a direct connection to a pipetting robot.
Mocking
Mocking is the simplest and fastest way to develop methods without having a connector deployed or a device available. This method uses a software mock that accepts all methods and provides default responses. It can be used to develop the first draft of a method before going into device testing. There is no internal state when using the mock, i.e. movement does not result in changes to responses regarding the current position of a channel for example. All states on the SDK side, such as labware, liquid levels, tip presences are still checked. Mocking is in beta.
Simulation
Running a connector or the pipetting software in a simulation mode. For this the connector is started in simulation mode or the pipetting robot software is set to a simulation mode. Setting the software of the pipetting robot to simulation mode can be done using the provided simulation mode feature. Keep in mind that connectors such as the Microlab STAR connector do not interact with the software, thus a simulation mode is provided within the connector. It can be activated by setting the environmental variable SIMULATION=True. Simulation is still in development.git st
Mocking
Importing the Mock object
When using a mock instance, no connection is made to the UniteLabs platform. A client object is not needed.
from unitelabs.testing.liquid_handling import MicrolabSTARMock
hamilton = MicrolabSTARMock()
We can already call basic commands to retrieve the firmware version and configuration of the mocked liquid handler:
>>> await hamilton.get_firmware_version()
'7.5S 19 2020_09_04 (GRU C0)'
>>> await hamilton.get_configuration()
{
'channel_order': False,
'collision_sensitivity': 'high',
'deck_track_count': 30,
'autoload_track_count': 30,
'waste_x': Decimal('800.0'),
'waste_direction': 'right',
'min_x': Decimal('350.0'),
'max_x': Decimal('1140.0'),
'left_arm_min_y': Decimal('6.0'),
'left_arm_max_y': Decimal('606.5'),
'left_arm_size': 'small',
'left_arm_width': Decimal('354.0'),
'right_arm_min_y': Decimal('6.0'),
'right_arm_max_y': Decimal('606.5'),
'right_arm_size': 'small',
'right_arm_width': Decimal('370.0'),
'pip_channels': 'left',
'pip_channel_count': 8,
'pip_channel_gap': Decimal('9.0'),
'pip_channel_volume': 1000,
'xl_channels': False,
'xl_channel_count': 0,
'xl_channel_gap': Decimal('36.0'),
'xl_channel_volume': 5000,
'robotic_channels': False,
'robotic_channel_count': 0,
'robotic_channel_gap': Decimal('36.0'),
'iswap': False,
'iswap_size': 'small',
'core96': False,
'core384': False,
'nano_pipettor': False,
'nano_pipettor_additional': False,
'tube_gripper': False,
'imaging_channel': False,
'gel_card_gripper': False,
'puncher': False,
'puncher_handler': False,
'autoload': False,
'wash_station_1': False,
'wash_station_1_type': 'G3',
'wash_station_2': False,
'wash_station_2_type': 'G3',
'pump_station_1': False,
'pump_station_2': False,
'pump_station_3': False,
'decapper_1': False,
'decapper_2': False,
'decapper_3': False,
'decapper_4': False,
'shaking_incubator_1': False,
'shaking_incubator_2': False,
'tcc_1': False,
'tcc_2': False,
'centrifuge': False,
'front_cover_monitoring': False,
'front_cover_monitoring_additional': False,
'left_cover': False,
'right_cover': False,
'autoload_cover': False,
}
Adjusting the device configuration
The device configuration is specific to each device and the modules it incorporates. The configuration stores not just the deck dimensions, but also the installed modules. The default configuration must be overwritten with the configuration of the device you are using. The best way to do this is by getting the output of the configuration of the real device and storing that away.
The default configuration for the mock is based on a Microlab STARlet configuration with only a few installed modules. Exchange this configuration with the configuration of your own device. This is important because several configurations are used for internal checks, such as the deck dimensions to validate movement operations.
We can overwrite the configuration by assigning a new configuration dictionary. Be aware that you have to reconfigure your hamilton object after changing the configuration object in order for the changes to take effect.
from decimal import Decimal
hamilton.configuration = await hamilton.get_configuration() | {
# Change Deck to STAR dimensions
'deck_track_count': 54,
'autoload_track_count': 54,
'waste_x': Decimal('1340.0'),
'max_x': Decimal('1140.0'),
# Enable custom modules
'iswap': 'left',
'core96': 'left',
'autoload': True,
}
await hamilton.configure()
We can always check the configuration by accessing the respective parameters directly:
>>> hamilton.configuration['deck_track_count']
54
>>> hamilton.configuration['iswap']
'left'
Initialization
The following command starts the initialization procedure. This can save valuable time during error recovery and avoid unnecessary initialization procedures. In the mocked environment await hamilton.is_initialized()
does not reflect the expected initialization status.
>>> await hamilton.initialize()
>>> await hamilton.pipettes.current_locations()
[
Vector(x=Decimal('1234.5'), y=Decimal('606.5'), z=Decimal('285.0')),
Vector(x=Decimal('1234.5'), y=Decimal('520.7'), z=Decimal('285.0')),
Vector(x=Decimal('1234.5'), y=Decimal('434.9'), z=Decimal('285.0')),
Vector(x=Decimal('1234.5'), y=Decimal('349.1'), z=Decimal('285.0')),
Vector(x=Decimal('1234.5'), y=Decimal('263.3'), z=Decimal('285.0')),
Vector(x=Decimal('1234.5'), y=Decimal('177.5'), z=Decimal('285.0')),
Vector(x=Decimal('1234.5'), y=Decimal('91.7'), z=Decimal('285.0')),
Vector(x=Decimal('1234.5'), y=Decimal('6.0'), z=Decimal('285.0')),
]
Running a movement command to a location that is within the configured deck dimensions will run without throwing any error.
from unitelabs.labware import Vector
await hamilton.pipettes[0].move_to(Vector(x=Decimal('100'), y=Decimal('100'), z=Decimal('300')))
Activating Modules
As long as a module is defined in the configuration, it can be activated and used.
await hamilton.iswap.activate()
To simulate initialization errors, or in case you have problems activating modules, the stage of each module can be changed manually.
from unitelabs.liquid_handling.modules import Stage
hamilton.configuration['iswap'] = True
hamilton.iswap._stage = Stage.CONFIGURED
await hamilton.iswap.initialize()
await hamilton.iswap.activate()
In conclusion, with a mock you can easily develop and test your automation with an actual device.
from decimal import Decimal
from unitelabs.labware import Vector
from unitelabs.testing.liquid_handling import MicrolabSTARMock
# Instantiate MicrolabSTAR Mock
hamilton = MicrolabSTARMock()
hamilton.configuration = await hamilton.get_configuration() | {
# Change Deck to STAR dimensions
'deck_track_count': 54,
'autoload_track_count': 54,
'waste_x': Decimal('1340.0'),
'max_x': Decimal('1140.0'),
# Enable custom modules
'iswap': 'left',
'core96': 'left',
'autoload': True,
}
await hamilton.configure()
await hamilton.initialize()
# Activate and use modules
await hamilton.iswap.activate()
await hamilton.iswap.move_to(Vector(x=Decimal('100'), y=Decimal('100'), z=Decimal('300')))