Dev Discussion: Heaters, Shakers, HeaterShakers and Coolers

Ohh that’s fantastic; that was a quick fix :sweat_smile:

Then I recommend we postpone HSB integration until someone has one and needs it integrated (at which point it would have risen in PLR integration priority).

I am looking forward to the Inheco cooler integrations of the controller and the physical definitions.
Please do share your thoughts on integrating them physcially by defining them as a MFXModule.

Yes. If anyone is wondering why we don’t merge the Resource and backend since both seem to follow a 1:1 relation, the opposite is actually illustrated in this thread: there are two ways to communicate with the same HHS (star or hamilton HSB multiplexer) and each way requires its own backend.

In addition, we should try to make the frontends (which are Resources) for each machine just different instances of the same high level class. For example, all liquid handlers are instances of LiquidHandler, and not an instance of some subclass of LiquidHandler. The specialized layout is given by the deck attribute. Similarly, all heater shakers should be instances of HeaterShaker and not of a subclass specific to one heater shaker. This helps hardware agnosticity, and ultimately reproducibility.

Hm, I see what you mean about reusing the site feature of MFXModule. But: all HeaterShakers would have to have such a site, so why should we not implement this functionality in the HeaterShaker Resource?

Now, this does mean that we have many resources all taking a single child at a specific site:

  • CarrierSite
  • HeaterShaker
  • MFXModule
  • Etc.

I think you were right earlier and this should be done in a single class. This pattern is so common in PLR modules that I think it is justified. I’m not sure I’d like this to be implemented in Resource itself, because some Resources (most notably Deck) should have the flexibility to take many child resources. We should create a new class like SingleChild(Resource) that essentially has the same implementation as MFXModule currently does, and make all of the above listed classes subclasses of that.

Perhaps a tangent because Hamilton_HS isn’t necessarily an MFXModule, but:
This is not really a constraint in PLR. Even though these resources are named MFXModule, we obviously don’t enforce a rule saying they can only be assigned to an MFXCarrier. People are free to directly assign the MFXModules like MFX_TIP_module to a Deck.

Why not go for the full name HamiltonHeaterShaker? People have auto complete set up in their code editors and this makes PLR more readable. Acronyms are bested avoided where possible imo.

def HamiltonHeaterShaker(backend: Union[STAR, FancyHamiltonMultiplexer]) -> HeaterShaker:
  return HeaterShaker(
    backend=backend,
    size_x=x,
    ...
  )

@jonlaurent I’m also trying to get two HHS working. So far I’ve plugged them into the ports on the left hand side of the Hamilton

and tried running:

await lh.backend.initialize_hhs(1)

Which results in:

ValueError: No Hamilton Heater Shaker found at device_number 1, have you checked your connections? Original error: Timeout while waiting for response to command T1QUid0012.

Did you have to do any firmware upgrades like @CamilloMoschner was mentioning, or did it work out of the box for you?

A little explanation:

I created this ValueError after testing out my HHS and HHC integration. Specifically, I wanted to see what happens when I do not plug the HHS in. The script was executing and waiting for a while and then threw up the “Original error”, i.e. the “T1QU” error. But because it isn’t really humanly interpretable and only ever appeared when I haven’t connected the HHS I wrote the ValueError and troubleshoot suggestion.

This means your error indicates that the connection to the HHS at TCC1 hasn’t been established.

I can see 3 potential reasons but will look more into it when I’m back at our machines:

  1. Have you checked that your HHS to STAR connection is tight? I found them sometimes to be a bit loose. (hence the error message reminder)

  2. You indeed need some firmware upgrade of unknown; I’m still hopeful that we don’t actually need that but honestly don’t know what and when Hamilton changes machine firmware.

  3. If you have connected the HHS plug into the STAR while the STAR was turned on, you might have fried the HHS. In that case the HHS is toast and wouldn’t be recognised by the STAR anymore. (Something I haven’t tested out myself :sweat_smile: but been told by an engineer multiple times)

After checking your connections, can you please confirm that neither of the two HHS initialisation commands is working, i.e. for neither of your two HHS?

2 Likes

Could it be you need to enable the “Temp. controlled carrier 1” / 2? See command AK, parameter kb (on STAR). A crappy PLR implementation exists already but you may wish to update it.

You can use RM / QM to get values for the other parameters if needed, they should match exactly.

(@CamilloMoschner could you please share the kb parameter of command RM on your machine?)

1 Like

Would you mind showing me the exact command I would need to make? :pray: I’m not quite super handy with the interface yet. :sweat_smile:

lol, quite a footgun. Thanks for your detailed explanation. Double and triple checked the connection, still no dice :upside_down_face:

could you share the outputs of C0RM and C0QM? check the doc to see which information it contains / feel free to DM

1 Like

Here is the command:

await lh.backend.send_command(module="C0", command="RM")

We should be able to figure out some more from the output of this command as Rick said above.

Yeah… not the best machine design imo :unamused:

1 Like

@rickwierenga, since this new class (that only takes one child resource at a pre-specified location, relative to itself) is exactly like MFXModule is right now, what would be the reason for creating a new class rather than just use MFXModule for all of them?
I could imagine maybe naming ambiguity?

…because I would disagree with this slightly: the HamiltonHeaterShaker is a MFXModule because it is designed to be screwed into a MFX_carrier. If you were to place it straight onto a flat lab bench and turn on the shaking it would wiggle off the bench quite quickly.
It requires some form of base to be screwed onto.
This base has to adhere to the dimensions defined by a MFXCarrier site.

This is very different to standalone heater-shakers like Eppendorf’s Thermomixers which are standalone machines that do not require a MFX_base.

Therefore, I think it would make sense that machines that are designed for fixing onto a MFX_carrier and take only a single child resource at a pre-defined location (relative to itself) are instances of MFXModule?

That is a good point. You changed my mind and I agree, HamiltonHeaterShaker is the way to go.

My question was going a bit further though:
How can we standardise the naming between the frontend (which I now understand to mean the physical definition of the machine) and the backend (the control system to operate the machine)?

E.g. at the moment we have

  • STAR architecture in PLR
    • backend: from pylabrobot.liquid_handling.backends import STAR
    • frontend: from pylabrobot.resources.hamilton import STARDeck
  • OT-2 architecture in PLR
    • backend: from pylabrobot.liquid_handling.backends import OpentronsBackend
    • frontend: from pylabrobot.resources.hamilton import OTDeck

This is for liquid_handlers and their decks. So the suffix “…Deck” nicely indicates that we are dealing with a frontend.
My question is how to set a similar naming standard for machines like heater-shakers, coolers, etc?

  • Hamilton Heater Shaker architecture in PLR
    • backend:
      1. Option 1: use =< 2 Hamilton Heater Shakers directly plugged into the STAR(let) → no backend needed because STAR itself acts as the backend to control the heater shaker(s) (already implemented)
      2. Option 2: use a standalone backend from pylabrobot.heater_shaking.backends import HamiltonHeaterShaker (not yet implemented)
    • frontend: from pylabrobot.resources.ml_star import ?

Because CarrierSite, all HeaterShakers, all PlateReaders, and presumably many other on-deck devices will all need this code. In other words, this behavior is not unique to MFXModule. Creating an abstraction seems like a good idea here. If HHS got this behavior from MFXModule (instead of HeaterShaker) that means other heater shakers, like the Inheco heater shaker, will not (automatically) get this behavior. If HHS got this single-plate-taking behavior from HeaterShaker, all heater shakers get it which seems desirable.

Good points. Let’s make it an MFXModule and HeaterShaker.

Technically, LiquidHandler is the front end here.

The front end should not be separate subclasses preferably. The single front end class (LiquidHandler, Scale, HeaterShaker, etc.) should have enough flexibility to support all devices. The way this flexibility works for LiquidHandler is that it takes a Deck to actually manage the resources ‘below’ it (in the tree). For other resources, such constructs are not necessarily needed.

I’d argue the STAR is the backend is this case.

Call this backend HSB? In general, we could use the Backend suffix for backends.

Thank you for the explanation. That makes a lot of sense: it is basically the search for “what is the biggest set possible to abstract towards”:
MFXModules or other resources that behave identical but are not necessarily designed for MFX_carriers.

Ohhh, I’m slowly piecing together what is meant here by “front end”:

A “front end” (in PLR) is given…

  1. a backend, to control the machine, and
  2. a physical definition of what this machine looks like in real life.

→ This means the “front end” controls function and physical positioning of a machine simultaneously?
(I am starting to see the analogy between this front end and the same term used in web dev)

This is very different from the opposing view of front end (physical definition) VS backend (control) I mistakenly had.


Going with the Backend suffix standard this would mean for the HHS an implementation could look like:

A “front end” called HeaterShaker (which @rickwierenga has already defined in pylabrobot/heatin_shaking) is given…

  1. a backend, to control the machine, called HamiltonHeaterShakerBackend and
  2. a physical definition of what this machine looks like in real life, called HamiltonHeaterShaker.

Looking at the documentation and the heater_shaker.py document this could be created using:

from pylabrobot.heating_shaking import HeaterShaker # front end
from pylabrobot.heating_shaking import HamiltonHeaterShakerBackend # backend
from pylabrobot.resources.ml_star import HamiltonHeaterShaker # physical definition/geometry/single child assignment information

heater_shaker_backend = HamiltonHeaterShakerBackend()
physical_definition = HamiltonHeaterShaker(name="HamiltonHeaterShaker")

hs_1 = HeaterShaker(
    backend = heater_shaker_backend,
    model = physical_definition 
)
await hs_1.setup()

?

In this situation, I am not quite sure what to give a name=?
In the documentation of the current implementation it is the HeaterShaker that is given a name but normally we name the resource. I could see use cases in which the physical shaker is used without control (as a simple plate holder).
To enable this, would it make sense to establish the physical_definition as above, with a name, and have the front end inherit the name from this physical_definition / model?

1 Like

This would mean that if one changes their Hamilton Heater Shaker with an Inheco Thermo Shaker they’d only have to change two lines in the above:

heater_shaker_backend = InhecoThermoShakeBackend()
physical_definition = InhecoThermoShake(name="InhecoThermoShake")

…embracing the PyLabRobot mantra of machine agnosticity :smiley:

1 Like

Quick question:

In this model of machines like heater shakers, we would still us the…

physical_definition = HamiltonHeaterShaker(name="HamiltonHeaterShaker")

…to move plate on/off the heater shaker, not the hs_1?

Heater shakers normally do not move around and it is generally not advisable, and oftentimes not practical, to manually move a plate onto them.
This means that their main use is dependent on plates being moved onto and off of them by the liquid handler, necessitating an easy and fast use of their “site”.
This is what we now achieved for the MFXModule in PR#77.
But for agnosticity purposes it would be best if the hs_1 variable inherits these characteristics from physical_definition.

Have decided to try and setup the HHS via the HSB unit.

Still investigating, but it seem like this would connect via USB and would make sense inherit from USBBackend, but it seems that USBBackend inherits from LiquidHandlerBackend? Is there a reason for this, or is it worth refactoring so that these are two separate classes?

2 Likes

Yes.

Note that LiquidHandler is a bit of a special case here, because of the Deck parameter. LiquidHandler itself is a Resource (see https://forums.pylabrobot.org/t/big-plr-update-visualizer-lh-is-a-resource/2981) but really its layout is modeled by a Deck. This is because there is much variance between decks. By having deck as a parameter, instead of doing all this work on the LiquidHandler itself, we can ensure that users always initialize the same class LiquidHandler. This helps with protocol agnosticity. The alternative would be many subclasses of LiquidHandler that manage the stuff that Deck currently manages, which would mean many different and potentially diverging definitions of what liquid handlers are.

For other devices, like plate readers and heater shakers, we don’t need this deck-as-a-child-resource-trick. We can simply manage the entire machine’s (=Resource) complexity in a single class, whether that’s PlateReader or HeaterShaker, etc. No deck parameter is needed because we can model the entire machine using parameters available on PlateReader and HeaterShaker.

(To complete the story - you already know this) In both cases, the machine takes a backend which will be responsible for sending commands to the machine. Backends implement minimal and atomic functions so it’s easy to add backends in the future. Minimal means that backends do not implement features that can be abstracted one level up, to the front ends. For example, for shakers, Shaker implements shake(speed: float) and stop_shaking. These can’t be split further and are easy to add for new backends. The Shaker front end introduces the duration: Optional[float] parameter (shake for a period of time), allowing the implementation (await asyncio.sleep(duration)) to be shared across all Shakers equally. Had we required backends to implement this, all backends would duplicate this code.

Yes.

Your views as described here (front end = physical definition, backend = control) are mostly right, but I would not label them as opposing. The front end does define the physical definition because they are the machines that exist in the layout tree, but the front ends also expose functions like aspirate that the user interacts with. Backends exist to implement functionality of the front end.

In the case of LiquidHandler, a separate and non-functional Deck exists to make sure we can have a robot-agnostic LiquidHandler class.

Preferably we get the entire physical definition into the front end. Machine is a Resource.

Interesting use case. I imagine it might be possible to initialize with a backend and not calling setup, or potentially backend=None.

I see how you see a physical_definition can be a way out, but intuitively I like having Machine as a Resource-subclass very nice. You share an interesting thought and it does change my intuition a bit, but I’m not convinced yet :slight_smile:

Adding a type hint should really reveal how good this can be:

heater_shaker_backend: HeaterShakerBackend = InhecoThermoShakeBackend()
my_heater_shaker: HeaterShaker = InhecoThermoShake(name="InhecoThermoShake", backend= heater_shaker_backend)

where InhecoThermoShake is just a function returning a HeaterShaker object initialized with the correct size_x,y,z and site.

This is very similar to how all PLR-plates are actually just instaces of Plate and resource definitions are simply stored as Python functions.

Exciting! Let’s use this double-backend/single-machine configuration to put our abstractions to the test.

You’re right, USBBackend should not be a LiquidHandlerBackend. Relic of a time when only liquid handlers were supported.

USBBackend should be for all Machines, and machines like STAR can double-inherit from LiquidHandlerBackend and USBBackend.

2 Likes

PR for USBBackend split here

2 Likes

I am not sure I follow. I thought HeaterShaker is the machine-agnostic frontend that is given the backend and specific geometry of the heater shaker to be used?

To brainstorm ideas of this implementation and keep an overview of different options it is always good to generate morphological charts. I’ve made one for this purpose here:

Since machines like this (that are designed to be fixed onto the liquid handler deck) depend on plate movement by the liquid handler my first question is, how can we command-wise quickly move plates onto the HeaterShaker?

i.e. how do we assign the HeaterShaker and its geometry to exact positions without having to declare a specific location=Coordinate(x,y,z) every time?
This is what we just solved with the MFXModule, hence why I thought this would be useful to adapt, or something similar to it.

1 Like

Correct.

The geometry is given as arguments directly to HeaterShaker, not as a separate Resource. HeaterShaker is the Resource.

>>> from pylabrobot.heating_shaking import HeaterShaker
>>> HeaterShaker.__init__
<function pylabrobot.heating_shaking.heater_shaker.HeaterShaker.__init__(self, name: str, size_x: float, size_y: float, size_z: float, backend: pylabrobot.heating_shaking.backend.HeaterShakerBackend, category: str = 'heating_shaking', model: Optional[str] = None)>

lh.move_plate(my_plate, my_heater_shaker)

If we make this SingleChildResource (which HeaterShaker inherits from), LH will automatically assign it to its site, similar to how MFXModule is currently implemented.

1 Like