Plate Reader Modelling Update

Hi everyone,

If I am not mistaken plate readers as currently modelled in PLR do not include the following machine features:

  • temperature control
  • shaking

However, most plate readers I have come across have both of these features (though, of course not all, as we will see soon here :eyes: )

pylabrobot/plate_reading/plate_reader.py:

class PlateReader(ResourceHolder, Machine):
  """The front end for plate readers. Plate readers are devices that can read luminescence,
  absorbance, or fluorescence from a plate.

  Plate readers are asynchronous, meaning that their methods will return immediately and
  will not block.

  Here's an example of how to use this class in a Jupyter Notebook:

  >>> from pylabrobot.plate_reading.clario_star import CLARIOStarBackend
  >>> pr = PlateReader(backend=CLARIOStarBackend())
  >>> pr.setup()
  >>> await pr.read_luminescence()
  [[value1, value2, value3, ...], [value1, value2, value3, ...], ...
  """

How does the community feel we can/should integrate these two features, starting with the BMG Labtech CLARIOstar?

until we have a better way to combine these functionalities, we should just have methods set_temperature get_temperature etc. we can inherit PlateReader from Shaker and TemperatureController if we feel most plate readers would reasonably support this as a temporary solution, but we could also just expose those methods on specific backends for now

1 Like

So it is an arguably subjective prioritisation question:

  1. see temperature control & shaking as integral features of PlateReader → have PlateReader inherit from TemperatureController and Shaker, and then have the “big three” features (luminescence, absorbance, fluorescence) added directly in PlateReader.

  2. see temperature control & shaking as unique features that are not seen as common amongst all plate readers. As a result, they are not accessible via PlateReader but are accessible via specific PlateReader backends instead. (A clear example in this category: some CLARIOstar’s have injector modules → quite unique amongst plate readers and therefore should be accessible via the backend but not the frontend)

1 Like

yeah

i think doing this through the backend is fine for now

2 Likes

I think inheriting from Shaker only makes sense if you can independently control the shaker functionality independent of run commands.

Generally, I propose we restrict control to keyword arguments (kwargs) passed from the front end directly to backend run commands, mirroring our existing approach for liquid handlers. Similar to liquid handlers, we can define sensible defaults specific to our backends and have a unified and predictable API across different hardware components.

For a more specific example, a device like the ClarioSTAR has several key shaker parameters, such as shake_when, rpm, shaker_type, and orbit_diameter.

For this kind of categorical settings with predefined limits, I strongly suggest using Enums from Python’s enum module.


Why Use Enums?

Using Enums will require users to import an object to use a given setting, but offers significant benefits that improve code quality and user experience:

  • Clarity: Enums make the code self-documenting. Using ShakerType.ORBITAL is much clearer than a raw string like "orbital", eliminating ambiguity. You can also provide annotation on the enum object itself, so, for example, it is not buried in a verbose run_absorbance doc-string.
  • Runtime Safety: Enums prevent errors by ensuring that only a predefined set of valid options can be passed. This stops bugs that could be caused by typos or unexpected values, making the system more robust.
  • Consistency: All possible settings are defined in a single location, which ensures everyone uses the same values and spelling throughout the codebase.
  • Discoverability: Code editors generally can auto-complete enum members, allowing developers to easily find all the available options without needing to check documentation.

I don’t know if it makes sense to refactor cases where we have Literals defined for this kind of categorical setting currently, but I think moving forward it might be good to use enums more generally. What are y’alls thoughts?

Just my 2c on enum vs literal:
I personaly prefere literals over many enum imports. They make the list of imports long (and I need to scroll up to add a new import when first used) and I need to find out which enum to use and from where to import in the first place. The arguments will often be longer ( MyEnum.Value instead of just “Value”), making the code more verbose overall which I find kind of unpythonic.

Modern IDE’s provide autocompletion for literal annotations and typecheckers basically provide the same safety for literals as for enums. In the error case, for enums the error will happen at the call site (unknown enum member), for literals the error will occur in the function when the literal is mapped to a firmware value so no big difference here.

3 Likes

agree on using strings, particularly for user-facing apis. PLR should be easily scriptable. also strings are more pythonic. (there are a couple of places where i don’t follow my own suggestion, like GripDirection. perhaps it’s time to change this.)

is this not the case for the clariostar?

this is not just about one device, but since it’s the front end it’s about “what plate reader is reasonably expected to support”


let’s use backend methods for now for shake+temperature on plate readers [until we figure out a nice way to combine devices with combined functionalities (platereader+shaker+temperaturecontroller, lh+arm etc.). separate discussion]

1 Like

option 2 of camillo’s summary: Plate Reader Modelling Update - #3 by CamilloMoschner

1 Like

I see and ultimately agree. I also realized if you wanted to say, pull values for a protocol from a config, it would unecessarily complicate parsing.

As far as I can tell from the control software, I don’t think you can run the shaker functionality independent of run commands involving plate readings.

2 Likes