Dosing Control - "Unable to find automation ..."

I’m trying to create a plugin and I’m running into this error message in the recent event logs.

Error
10:50:28 pioreactor1 dosing control Unable to find automation multi_media_chemostat. Available automations are [‘chemostat’, ‘continuous_cycle’, ‘fed_batch’, ‘morbidostat’, ‘pid_morbidostat’, ‘silent’, ‘turbidostat’, ‘alt_chemostat’]

This occurs when I go to pioreactor1.local/pioreactors → [Manage] → [Change Dosing Automation] → (dropdown) [my new plugin]

I checked pioreactor1.local/plugins and my new plugin (“Multi Media Chemostat Automation”) is shown under “Installed Plugins”. I refreshed the browser and when I navigate to [Change Dosing Automation] (above), I can select my automation (“Multi Media Chemostat”), configure my settings, and then press “START”.

I’m not sure why I’m getting this error. My plugin is showing up in both pioreactor1.local/pioreactors, which means it has found my .py file, and it is showing up in the UI list of automations, which means it has found my .yaml file. My .py file says automation_name = "multi_media_chemostat" and my .yaml file says automation_name: multi_media_chemostat so I am not sure why it is unable to find my automation.

/var/log/pioreactor.log

Traceback (most recent call last):
File “/usr/local/lib/python3.9/dist-packages/pioreactor/background_jobs/dosing_control.py”, line 104, in set_automation
klass = self.available_automations[algo_metadata.automation_name]
KeyError: ‘multi_media_chemostat’
2023-02-23T11:10:43-0800 [dosing_control] WARNING Unable to find automation multi_media_chemostat. Available automations are [‘chemostat’, ‘continuous_cycle’, ‘fed_batch’, ‘morbidostat’, ‘pid_morbidostat’, ‘silent’, ‘turbidostat’, ‘alt_chemostat’]
2023-02-23T11:10:43-0800 [dosing_control] INFO Updated automation from alt_chemostat(skip_first_run=0, media_volume=0.5, alt_media_volume=0.1, duration=60) to alt_chemostat(skip_first_run=0, media_volume=0.5, alt_media_volume=0.1, duration=60).

~/.pioreactor/plugins/ui/contrib/automations/dosing/multi_media_chemostat.yaml

---
display_name: Multi Media Chemostat
automation_name: multi_media_chemostat
description: Testing for now.
fields:
  - key: media_volume
    unit: mL
    label: Volume of media added.
    default: 0.5
  - key: media_duration
    unit: min
    label: Interval between media additions.
    default: 20
  - key: alt_media_volume
    unit: mL
    label: Volume of alternative media added.
    default: 0.1
  - key: alt_media_duration
    unit: min
    label: Interval between alt_media additions.
    default: 60
  - key: duration
    unit: min
    label: Interval to wake up. Default to 1.
    default: 1

~/.pioreactor/plugins/multi_media_chemostat.py

# -*- coding: utf-8 -*-

from __future__ import annotations

from pioreactor.automations import events
from pioreactor.automations.dosing.base import DosingAutomationJobContrib
from pioreactor.exc import CalibrationError
from pioreactor.utils import local_persistant_storage

__plugin_summary__ = "An extension of the chemostat automation to allow a second media source."
__plugin_version__ = "0.0.1"
__plugin_name__ = "Multi Media Chemostat Automation"
__plugin_author__ = "realPeteDavidson"
__plugin_homepage__ = "https://docs.pioreactor.com"

class MultiMediaChemostat(DosingAutomationJobContrib):
    """
    Alternate Chemostat mode - try to keep [media] and [alt_media] constant.
    """

    automation_name = "multi_media_chemostat"
    published_settings = {
            "media_volume": {"datatype": "float", "settable": True, "unit": "mL"},
            "duration": {"datatype": "float", "settable": True, "unit": "min"},
            "alt_media_volume": {"datatype": "float", "settable": True, "unit": "mL"},
            "media_duration": {"datatype": "float", "settable": True, "unit": "min"},
            "alt_media_duration": {"datatype": "float", "settable": True, "unit": "min"},
            }

    def __init__(self, media_volume: float | str, alt_media_volume: float | str, media_duration: float | str, alt_media_duration : float | str, **kwargs) -> None:
        super().__init__(**kwargs)

        with local_persistant_storage("current_pump_calibration") as cache:
            if "media" not in cache:
                raise CalibrationError("Media pump calibration must be performed first.")
            elif "waste" not in cache:
                raise CalibrationError("Waste pump calibration must be performed first.")
            elif "alt_media" not in cache:
                raise CalibrationError("Alt_Media pump calibration must be performed first.")

        self.media = float(media_volume)
        self.alt_media = float(alt_media_volume)
        self.media_duration = float(media_duration)
        self.alt_media_duration = float(alt_media_duration)
        self.media_counter = 0
        self.alt_media_counter = 0

    def execute(self) -> events.DilutionEvent:
        media_ml = 0
        alt_media_ml = 0
        waste_ml = 0
        if self.media_counter >= self.media_duration:
            media_ml = self.media
            self.media_counter -= self.media_duration
        if self.alt_media_counter >= self.alt_media_duration:
            alt_media_ml = self.alt_media
            self.alt_media_counter -= self.alt_media_duration
        waste_ml = media_ml + alt_media_ml
        volume_actually_cycled = self.execute_io_action(media_ml, alt_media_ml, waste_ml)
        self.media_counter += self.duration
        self.alt_media_counter += self.duration
		return events.DilutionEvent(
                f"exchanged {volume_actually_cycled['waste_ml']}mL",
                data={"volume_actually_cycled": volume_actually_cycled["waste_ml"]},
                )

It sounds like you’re starting a dosing automation (one that is not multi_media_chemostat), and then changing it to multi_media_chemostat - is that correct?

I can’t see anything that’s obviously wrong. Can you try the following from the command line, just to confirm?

> pio run dosing_control --automation-name "multi_media_chemostat" --media_volume 0 --alt_media_volume 0 --media_duration 0  --alt_media_duration 0

Does that run? You can ctrl-c if it does.

(actually there is a logic bug in the Python code you provided, 'waste_ml' is not defined, but that seems unrelated to the original issue).

It sounds like you’re starting a dosing automation (one that is not multi_media_chemostat ), and then changing it to multi_media_chemostat - is that correct?
Yes, that was the issue. I had assumed I could use “Change Dosing Automation” to not only change the “published_settings”, but also to change from any one automation to any other automation.

I just stopped the currently running dosing automation, then I started a new dosing automation for “multi media chemostat”. This time it worked and didn’t give me the error. The only thing I did differently (aside from the waste_ml bugfix) was turn the dosing automation on and off. I am now also able to use “Change Dosing Automation” to change to/from the “multi media chemostat” automation without error.

That seemed to be the fix. Maybe the list of dosing automations was tied to the dosing controller when it was instantiated? So when I turned it on/off it would have gotten updated?

Also, thanks for catching the bug. I updated the original post with the fixed code.

Also, is there any way to order the different fields in the .yaml for how they appear in the UI/automation?

Ah yes that is what happens! I’ll make a note of this to add to our docs.

Also, is there any way to order the different fields in the .yaml for how they appear in the UI/automation?

Kinda. The sorting code is here. From that logic:

  • yaml in the ~/.pioreactors/... come last.
  • you could prepend the file names with 01_...yaml, 02_...yaml, etc., which would force an ordering, like we do for jobs. Renaming these files is harmless.

However, chemostat is hardcoded as the default as the first that appears when you open the modal.


Ex: if you want your new plugin first in the dropdown, you could move it to /var/www/pioreactorui/contrib/automations/dosing and call it 01_multimedia_chemostat.yaml.

That’s good to know (and may have been one of my next questions). I was actually asking about the order of the published settings. For example, if you select the Multi Media Chemostat in the dropdown menu, the settings look like this

[media_volume] | [media_duration] | [alt_media_volume]
[alt_media_duration] | [duration]

Right now, it just followings
[setting 1] | [setting 2] | [setting 3]
[setting 4] | [setting 5] | [setting 6]
… and so on

Is there any way to organize these settings?
Like have media settings on one row, alt media settings on row 2, and the last setting on row 3?

ooo I think if you change the order in the fields in the yaml, that would change the order in the UI. But there’s nothing beyond that.

Ok. Are there any other fields that you can put in an automation .yaml?

I think I saw a “source:” field for folder location in an example somewhere, but I haven’t been using that field. When would I need that field? If I don’t place my plugin in ~/.pioreactor/plugins/?

Here’s the description of what a automation yaml should look like: pioreactorui/structs.py at master · Pioreactor/pioreactorui · GitHub, and related is the description of a field: pioreactorui/structs.py at master · Pioreactor/pioreactorui · GitHub


Reading this now, I should include “source” in the first description, which would represent which plugin added this job / automation, but it’s only cosmetic.

Thanks. I saw it here. Just to clarify, if I try adding a “source” field to my automation.yaml, it would return an error because “source” is not (listed? defined?) in class AutomationDescriptor, and if you added the source field, the source field would only be cosmetic?

Ah, if you added source: whatever to your yaml, it would be an error because your yaml doesn’t conform to the spec (source is not defined in the current spec).

My footnote referred to adding source: str to the spec, so it can be populated in yamls. Similar to BackgroundJobDescriptor in that same file.

1 Like

The cosmetic part referred to displaying the source like this:

1 Like

Thanks. Hopefully this is my last question for a bit.

I’ve been looking through the f-strings link you sent me a while ago and I wanted to make a small change to the DilutionEvents.

Currently, it is:

return events.DilutionEvent(
    f"exchanged {volume_actually_cycled['waste_ml']}mL",
    data={"volume_actually_cycled": volume_actually_cycled["waste_ml"]},
    )

Assume I add 1 mL media, 0.5 mL alt_media, and remove 1.5 mL waste.
It returns this in the event log :
DilutionEvent: exchanged 1.5mL

I want it to return this (for bug testing):
DilutionEvent: exchanged [1mL, 0.5mL, 1.5mL]

I think it should look something like this:

return events.DilutionEvent(
    f"exchanged [{volume_actually_cycled['media_ml']}mL, {volume_actually_cycled['alt_media_ml']}mL, {volume_actually_cycled['waste_ml']}mL]",
    data={"volume_actually_cycled": volume_actually_cycled["waste_ml"]},
    )

I suspect that I should be editing the data dict, but I’m having difficulty figuring out how it should be expanded.

Something like this would work:

return events.DilutionEvent(
    f"exchanged [{volume_actually_cycled['media_ml']}mL, {volume_actually_cycled['alt_media_ml']}mL, {volume_actually_cycled['waste_ml']}mL]",
    data={"volume_actually_cycled": volume_actually_cycled["waste_ml"]},

Thanks. I wanted to look at how you structured the temperature cycle automation. I wasn’t able to find the code for the automation in your experiment posts, or on the pioreactor1.local/plugins webpage. Is there somewhere I can find the code?

Here it is: CycleTemp.py · GitHub

It’s not very intuitive how it works. It relies on inheritance from Stable, and uses Stables logic to do the targeting.

Edit: I mean Thermostat, not Stable

I think I get what is going on in the code, but I’m not sure what you meant by inheritance from Stable.

Is this what you’re referring to as Stables logic?

def get_new_target_temperature(self):
        current_seconds_in_day = time.time() % (60 * 60 * 24)

        return self.bias_temperature + self.amplitude * sin(2 * pi * current_seconds_in_day  / (60 * 60 * 24))

Ah my bad, Thermostat use to be called Stable. I meant Thermostat

1 Like