Byonoy Absorbance 96 Automate#

  • OEM Link

  • Communication Protocol / Hardware: HID / USB-A/C

  • Communication Level: Firmware

  • VID:PID 16d0:1199

  • Takes a single SLAS-format 96-wellplate on the detection unit, enables movement of the cap/illumination unit over it, and reads all 96 wells simultaneously.

  • Up to 6 configurable absorbance wavelengths (dependent on specifications during purchase).


Setup Instructions (Physical)#

The Byonoy Absorbance 96 Automate (A96A) is a an absorbance plate reader consisting of…

  1. a detection_unit containing the liqht sensors,

  2. a illumination_unit containing the light source,

  3. a parking_unit representing a simple resource_holder for the illumination_unit, and

  4. an sbs_adapter which is an optional holder for the detection_unit or parking_unit, enabling placement of this machine onto a standard SLAS/SBS-format plate holder.

Communication#

It requires only one cable connections to be operational:

  1. USB cable (USB-C at base end; USB-A at control PC end)


Setup Instructions (Programmatic)#

If used with a liquid handler, first setup the liquid handler:

from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerChatterboxBackend
from pylabrobot.resources import STARDeck

lh = LiquidHandler(deck=STARDeck(), backend=LiquidHandlerChatterboxBackend())
from pylabrobot.resources import (
    hamilton_mfx_carrier_L5_base, MFX_CAR_L4_SHAKER , # MFX CARRIERS
    MFX_DWP_rackbased_module, hamilton_mfx_plateholder_DWP_metal_tapped,
)
# 

mfx_carrier_2_plateholders = hamilton_mfx_carrier_L5_base(
  name="mfx_carrier_2_plateholders",
  modules={
      4: hamilton_mfx_plateholder_DWP_metal_tapped(name=f"mfx_plateholder_1"),
      2: hamilton_mfx_plateholder_DWP_metal_tapped(name=f"mfx_plateholder_parking_unit"),
      0: hamilton_mfx_plateholder_DWP_metal_tapped(name=f"mfx_plateholder_detection_unit")
  }
)

lh.deck.assign_child_resource(mfx_carrier_2_plateholders, rails=12)
await lh.setup()
Setting up the liquid handler.

Then generate a plate definition for the plate you want to read:

from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources.cellvis.plates import CellVis_96_wellplate_350uL_Fb


demo_plate = CellVis_96_wellplate_350uL_Fb(name='demo_plate')

mfx_carrier_2_plateholders[4] = demo_plate
from pylabrobot.plate_reading.byonoy import (
    byonoy_sbs_adapter,
    byonoy_a96a_detection_unit, 
    byonoy_a96a_illumination_unit,
    byonoy_a96a_parking_unit
)
# Detection Unit
a96a_sbs_adapter_DU = byonoy_sbs_adapter(name="a96a_sbs_adapter_DU")
a96a_detection_unit = byonoy_a96a_detection_unit(name="a96a_detection_unit")
a96a_sbs_adapter_DU.assign_child_resource(a96a_detection_unit)

mfx_carrier_2_plateholders[0] = a96a_sbs_adapter_DU

# Parking Unit
a96a_sbs_adapter_PU = byonoy_sbs_adapter(name="a96a_sbs_adapter_PU")
a96a_parking_unit = byonoy_a96a_parking_unit(name="a96a_parking_unit")
a96a_sbs_adapter_PU.assign_child_resource(a96a_parking_unit)

mfx_carrier_2_plateholders[2] = a96a_sbs_adapter_PU


a96a_illumination_unit = byonoy_a96a_illumination_unit(name="a96a_illumination_unit")
a96a_detection_unit.assign_child_resource(a96a_illumination_unit)
Resource 'a96a_illumination_unit' is very high on the deck: 257.948 mm. Be careful when traversing the deck.
a96a_detection_unit.get_location_wrt(lh.deck)
Coordinate(x=337.75, y=67.0, z=200.95)
lh.summary()
Rail  Resource                          Type                 Coordinates (mm)
===========================================================================================
(-6)  ├── trash_core96                  Trash                (-58.200, 106.000, 216.400)
      │
(12)  ├── mfx_carrier_2_plateholders    MFXCarrier           (347.500, 063.000, 100.000)
      │   ├── demo_plate                Plate                (351.500, 456.000, 182.050)
      │   ├── a96a_sbs_adapter_PU       ResourceHolder       (351.500, 264.000, 183.950)
      │   │   ├── a96a_parking_unit     ByonoyBaseUnit       (337.750, 259.000, 200.950)
      │   ├── a96a_sbs_adapter_DU       ResourceHolder       (351.500, 072.000, 183.950)
      │   │   ├── a96a_detection_unit   ByonoyBaseUnit       (337.750, 067.000, 200.950)
      │
(55)  ├── waste_block                   Resource             (1315.000, 115.000, 100.000)
      │   ├── teaching_tip_rack         TipRack              (1320.900, 461.100, 100.000)
      │   ├── core_grippers             HamiltonCoreGrippers (1337.500, 125.000, 205.000)
      │
(56)  ├── trash                         Trash                (1340.000, 190.600, 137.100)
lh.summary()
Rail  Resource                              Type                 Coordinates (mm)
===============================================================================================
(-6)  ├── trash_core96                      Trash                (-58.200, 106.000, 216.400)
      │
(12)  ├── mfx_carrier_2_plateholders        MFXCarrier           (347.500, 063.000, 100.000)
      │   ├── demo_plate                    Plate                (351.500, 456.000, 182.050)
      │   ├── a96a_sbs_adapter_PU           ResourceHolder       (351.500, 264.000, 183.950)
      │   │   ├── a96a_parking_unit         ByonoyBaseUnit       (337.750, 259.000, 200.950)
      │   ├── a96a_sbs_adapter_DU           ResourceHolder       (351.500, 072.000, 183.950)
      │   │   ├── a96a_detection_unit       ByonoyBaseUnit       (337.750, 067.000, 200.950)
      │   │   │   ├── a96a_illumination_unitResource             (337.750, 067.000, 215.050)
      │
(55)  ├── waste_block                       Resource             (1315.000, 115.000, 100.000)
      │   ├── teaching_tip_rack             TipRack              (1320.900, 461.100, 100.000)
      │   ├── core_grippers                 HamiltonCoreGrippers (1337.500, 125.000, 205.000)
      │
(56)  ├── trash                             Trash                (1340.000, 190.600, 137.100)
await lh.move_resource(a96a_illumination_unit, a96a_parking_unit, pickup_distance_from_top=13.5)
Resource 'a96a_illumination_unit' is very high on the deck: 257.948 mm. Be careful when traversing the deck.
Picking up resource: ResourcePickup(resource=Resource(name='a96a_illumination_unit', location=Coordinate(000.000, 000.000, 014.100), size_x=155.26, size_y=95.48, size_z=42.898, category=None), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=13.5, direction=<GripDirection.FRONT: 1>)
Dropping resource: ResourceDrop(resource=Resource(name='a96a_illumination_unit', location=Coordinate(000.000, 000.000, 014.100), size_x=155.26, size_y=95.48, size_z=42.898, category=None), destination=Coordinate(x=337.75, y=259.0, z=200.95), destination_absolute_rotation=Rotation(x=0, y=0, z=0), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=13.5, pickup_direction=<GripDirection.FRONT: 1>, direction=<GripDirection.FRONT: 1>, rotation=0)
lh.summary()
Rail  Resource                              Type                 Coordinates (mm)
===============================================================================================
(-6)  ├── trash_core96                      Trash                (-58.200, 106.000, 216.400)
      │
(12)  ├── mfx_carrier_2_plateholders        MFXCarrier           (347.500, 063.000, 100.000)
      │   ├── <empty>
      │   ├── a96a_sbs_adapter_PU           ResourceHolder       (351.500, 264.000, 183.950)
      │   │   ├── a96a_parking_unit         ByonoyBaseUnit       (337.750, 259.000, 200.950)
      │   │   │   ├── a96a_illumination_unitResource             (337.750, 259.000, 215.050)
      │   ├── a96a_sbs_adapter_DU           ResourceHolder       (351.500, 072.000, 183.950)
      │   │   ├── a96a_detection_unit       ByonoyBaseUnit       (337.750, 067.000, 200.950)
      │
(55)  ├── waste_block                       Resource             (1315.000, 115.000, 100.000)
      │   ├── teaching_tip_rack             TipRack              (1320.900, 461.100, 100.000)
      │   ├── core_grippers                 HamiltonCoreGrippers (1337.500, 125.000, 205.000)
      │
(56)  ├── trash                             Trash                (1340.000, 190.600, 137.100)
await lh.move_plate(demo_plate, a96a_detection_unit)
Picking up resource: ResourcePickup(resource=Plate(name='demo_plate', size_x=127.6, size_y=85.75, size_z=13.83, location=None), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=9.87, direction=<GripDirection.FRONT: 1>)
Dropping resource: ResourceDrop(resource=Plate(name='demo_plate', size_x=127.6, size_y=85.75, size_z=13.83, location=None), destination=Coordinate(x=337.75, y=67.0, z=200.95), destination_absolute_rotation=Rotation(x=0, y=0, z=0), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=9.87, pickup_direction=<GripDirection.FRONT: 1>, direction=<GripDirection.FRONT: 1>, rotation=0)
lh.summary()
Rail  Resource                              Type                 Coordinates (mm)
===============================================================================================
(-6)  ├── trash_core96                      Trash                (-58.200, 106.000, 216.400)
      │
(12)  ├── mfx_carrier_2_plateholders        MFXCarrier           (347.500, 063.000, 100.000)
      │   ├── <empty>
      │   ├── a96a_sbs_adapter_PU           ResourceHolder       (351.500, 264.000, 183.950)
      │   │   ├── a96a_parking_unit         ByonoyBaseUnit       (337.750, 259.000, 200.950)
      │   │   │   ├── a96a_illumination_unitResource             (337.750, 259.000, 215.050)
      │   ├── a96a_sbs_adapter_DU           ResourceHolder       (351.500, 072.000, 183.950)
      │   │   ├── a96a_detection_unit       ByonoyBaseUnit       (337.750, 067.000, 200.950)
      │   │   │   ├── demo_plate            Plate                (360.250, 072.000, 216.950)
      │
(55)  ├── waste_block                       Resource             (1315.000, 115.000, 100.000)
      │   ├── teaching_tip_rack             TipRack              (1320.900, 461.100, 100.000)
      │   ├── core_grippers                 HamiltonCoreGrippers (1337.500, 125.000, 205.000)
      │
(56)  ├── trash                             Trash                (1340.000, 190.600, 137.100)
await lh.move_resource(a96a_illumination_unit, a96a_detection_unit, pickup_distance_from_top=13.5)
Resource 'a96a_illumination_unit' is very high on the deck: 257.948 mm. Be careful when traversing the deck.
Picking up resource: ResourcePickup(resource=Resource(name='a96a_illumination_unit', location=Coordinate(000.000, 000.000, 014.100), size_x=155.26, size_y=95.48, size_z=42.898, category=None), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=13.5, direction=<GripDirection.FRONT: 1>)
Dropping resource: ResourceDrop(resource=Resource(name='a96a_illumination_unit', location=Coordinate(000.000, 000.000, 014.100), size_x=155.26, size_y=95.48, size_z=42.898, category=None), destination=Coordinate(x=337.75, y=67.0, z=200.95), destination_absolute_rotation=Rotation(x=0, y=0, z=0), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=13.5, pickup_direction=<GripDirection.FRONT: 1>, direction=<GripDirection.FRONT: 1>, rotation=0)
lh.summary()
Rail  Resource                              Type                 Coordinates (mm)
===============================================================================================
(-6)  ├── trash_core96                      Trash                (-58.200, 106.000, 216.400)
      │
(12)  ├── mfx_carrier_2_plateholders        MFXCarrier           (347.500, 063.000, 100.000)
      │   ├── <empty>
      │   ├── a96a_sbs_adapter_PU           ResourceHolder       (351.500, 264.000, 183.950)
      │   │   ├── a96a_parking_unit         ByonoyBaseUnit       (337.750, 259.000, 200.950)
      │   ├── a96a_sbs_adapter_DU           ResourceHolder       (351.500, 072.000, 183.950)
      │   │   ├── a96a_detection_unit       ByonoyBaseUnit       (337.750, 067.000, 200.950)
      │   │   │   ├── demo_plate            Plate                (360.250, 072.000, 216.950)
      │   │   │   ├── a96a_illumination_unitResource             (337.750, 067.000, 215.050)
      │
(55)  ├── waste_block                       Resource             (1315.000, 115.000, 100.000)
      │   ├── teaching_tip_rack             TipRack              (1320.900, 461.100, 100.000)
      │   ├── core_grippers                 HamiltonCoreGrippers (1337.500, 125.000, 205.000)
      │
(56)  ├── trash                             Trash                (1340.000, 190.600, 137.100)
await lh.move_resource(demo_plate, mfx_carrier_2_plateholders[4], pickup_distance_from_top=13.5)
Picking up resource: ResourcePickup(resource=Plate(name='demo_plate', size_x=127.6, size_y=85.75, size_z=13.83, location=Coordinate(022.500, 005.000, 016.000)), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=13.5, direction=<GripDirection.FRONT: 1>)
Dropping resource: ResourceDrop(resource=Plate(name='demo_plate', size_x=127.6, size_y=85.75, size_z=13.83, location=Coordinate(022.500, 005.000, 016.000)), destination=Coordinate(x=351.5, y=456.0, z=182.05), destination_absolute_rotation=Rotation(x=0, y=0, z=0), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=13.5, pickup_direction=<GripDirection.FRONT: 1>, direction=<GripDirection.FRONT: 1>, rotation=0)
demo_plate.get_location_wrt(lh.deck)
Coordinate(x=360.25, y=72.0, z=216.95)
a96a_detection_unit.get_location_wrt(lh.deck)
Coordinate(x=337.75, y=67.0, z=200.95)
a96a_detection_unit.get_location_wrt(lh.deck)+ a96a_detection_unit.child_location
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[16], line 1
----> 1 a96a_detection_unit.get_location_wrt(lh.deck)+ a96a_detection_unit.child_location

AttributeError: 'ByonoyBaseUnit' object has no attribute 'child_location'
a96a_detection_unit.assign_child_resource(a96a_illumination_unit)
lh.summary()
[print(x) for x in a96a_sbs_adapter.children]
ByonoyA96ABaseUnit(name='a96a_detection_unit', location=Coordinate(-13.750, -05.000, 017.000), size_x=155.26, size_y=95.48, size_z=18.5, category=resource_holder)
[None]
[print(x) for x in a96a_detection_unit.children]
Plate(name='demo_plate', size_x=127.6, size_y=85.75, size_z=13.83, location=Coordinate(022.500, 005.000, 016.000))
Resource(name='a96a_illumination_unit', location=Coordinate(000.000, 000.000, 014.100), size_x=155.26, size_y=95.48, size_z=42.898, category=None)
[None, None]

Now instantiate the Byonoy absorbance plate reader:

from pylabrobot.plate_reading.byonoy import (
    byonoy_absorbance_adapter,
    byonoy_absorbance96_base_and_reader
)

cap_adapter = byonoy_absorbance_adapter(name='cap_adapter')

base, reader_cap = byonoy_absorbance96_base_and_reader(name='base', assign=True)

lh.deck.assign_child_resource(cap_adapter, location=Coordinate(400, 0, 0))
await reader_cap.setup(verbose=True)

reader_cap.setup_finished
Connected to Bynoy Absorbance 96 Automate (via HID with VID=5840:PID=4505) on b'DevSrvsID:4308410804'
Identified available wavelengths: [420, 600] nm
True
reader_cap.backend.io.device_info
{'path': b'DevSrvsID:4308410804',
 'vendor_id': 5840,
 'product_id': 4505,
 'serial_number': 'BYOMAA00058',
 'release_number': 512,
 'manufacturer_string': 'Byonoy GmbH',
 'product_string': 'Absorbance 96 Automate',
 'usage_page': 65280,
 'usage': 1,
 'interface_number': 0,
 'bus_type': <BusType.USB: 1>}
reader_cap.backend.available_wavelengths
[420, 600]

Test Movement for Plate Reading#

cap_adapter, base, reader_cap
(ResourceHolder(name='cap_adapter', location=Coordinate(400.000, 000.000, 000.000), size_x=127.76, size_y=85.59, size_z=14.07, category=resource_holder),
 ByonoyBase(name='base_base', location=None, size_x=138, size_y=95.7, size_z=27.7, category=None),
 PlateReader(name='base_reader', location=Coordinate(000.000, 000.000, 010.660), size_x=138, size_y=95.7, size_z=0, category=None))

Usage / Machine Features#

Query Machine Configuration#

await reader_cap.backend.get_available_absorbance_wavelengths()
[420, 600]

Measure Absorbance#

readings_420_nested_list = await reader_cap.backend.read_absorbance(
    wells=plate.children[:55],
    wavelength = 420, # units: nm
    output_nested_list=True
)

import pandas as pd

pd.DataFrame(readings_420_nested_list)
0 1 2 3 4 5 6 7 8 9 10 11
0 0.000002 -0.000002 0.000083 0.000038 0.000048 2.975314e-05 0.000075 None None None None None
1 0.000062 0.000051 0.000040 0.000018 0.000064 3.082320e-05 0.000044 None None None None None
2 0.000088 0.000055 0.000069 0.000009 0.000079 7.937726e-05 0.000078 None None None None None
3 0.000080 0.000050 0.000009 0.000069 0.000067 3.182423e-05 0.000070 None None None None None
4 0.000042 0.000003 0.000110 0.000005 -0.000005 -1.815412e-05 0.000070 None None None None None
5 0.000055 0.000054 -0.000023 0.000041 0.000036 9.664112e-07 0.000039 None None None None None
6 0.000046 0.000025 0.000019 0.000017 0.000039 3.658781e-05 0.000066 None None None None None
7 0.000038 0.000018 0.000055 0.000041 0.000034 -3.216584e-05 NaN None None None None None
import time
start_time = time.time()

readings_600_nested_list = await reader_cap.backend.read_absorbance(
    wells=plate.children[:],
    wavelength = 600, # units: nm
    output_nested_list=True
)
display(pd.DataFrame(readings_600_nested_list))


time.time() - start_time
0 1 2 3 4 5 6 7 8 9 10 11
0 0.000097 0.000079 0.000087 0.000092 0.000085 0.000097 0.000086 0.000088 0.000074 0.000111 0.000066 0.000076
1 0.000050 0.000074 0.000063 0.000054 0.000073 0.000066 0.000050 0.000061 0.000082 0.000095 0.000051 0.000059
2 0.000093 0.000049 0.000031 0.000081 0.000067 0.000083 0.000066 0.000104 0.000074 0.000064 0.000040 0.000069
3 0.000096 0.000074 0.000023 0.000075 0.000100 0.000053 0.000064 0.000087 0.000070 0.000073 0.000050 0.000054
4 0.000087 0.000074 0.000161 0.000070 0.000080 0.000069 0.000101 0.000106 0.000112 0.000103 0.000059 0.000062
5 0.000058 0.000067 0.000023 0.000068 0.000036 0.000053 0.000035 0.000044 0.000045 0.000097 0.000039 0.000033
6 0.000080 0.000036 0.000012 0.000079 0.000062 0.000061 0.000046 0.000084 0.000043 0.000050 0.000026 0.000064
7 0.000087 0.000053 0.000072 0.000060 0.000076 0.000031 0.000034 0.000084 0.000086 0.000054 0.000032 0.000079
1.5100939273834229
start_time = time.time()

readings_600_nested_list = await reader_cap.backend.read_absorbance(
    wells=plate.children[:],
    wavelength = 600, # units: nm
    output_nested_list=True
)
display(pd.DataFrame(readings_600_nested_list))

time.time() - start_time
first_n_columns = 8

readings_420 = await reader_cap.backend.read_absorbance(
    wells=plate.children[:8*first_n_columns],
    wavelength = 420 # units: nm
)
readings_600 = await reader_cap.backend.read_absorbance(
    wells=plate.children[:8*first_n_columns],
    wavelength = 600 # units: nm
)

well_indexed_df = pd.DataFrame([readings_420, readings_600], index=["420nm", "600nm"]).T
well_indexed_df
420nm 600nm
A1 0.000064 0.000100
B1 0.000097 0.000033
C1 0.000165 0.000086
D1 0.000105 0.000082
E1 0.000106 0.000132
... ... ...
D8 0.000073 0.000117
E8 0.000085 0.000107
F8 0.000057 0.000053
G8 0.000124 0.000102
H8 0.000079 0.000128

64 rows × 2 columns

Disconnect from Reader#

await reader_cap.stop()