Error when passing variable/list through DosingController to custom automation

I am trying to implement a schedule that lets me control when and how much alternative media is added to my pioreactor, but I seem to be having an issue with defining the variable and passing it through DosingController to my custom class (at least I think that is what is going wrong).

I run the code by instantiating dc = DosingController( …, duration = 20, array=[[5, 10, 20, 40, 60, 80], [0.5, 0, 1.0, 0, 1.5, 0]], … ), where array is a list in the format:
array[0][:] # when the change is supposed to take place
array[1][:] # the new dosage at that point in the schedule. Currently these are test/debug values.

I am getting an error saying ERROR [app] [dosing_automation] name 'alt_dosing_schedule' is not defined and I am not sure how to resolve this. Can I resolve this issue by modifying chemostat_alt_media_2.py, or will I have to make this fix somewhere in pioreactor/automations/dosing/base.py DosingAutomationJobContrib?

chemostat_alt_media_2.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
 

class ChemostatAltMedia(DosingAutomationJobContrib):
    """
    Chemostat mode - try to keep [nutrient] constant.
    """

    automation_name = "scheduled_chemostat_alt_media"
    published_settings = {
        "volume": {"datatype": "float", "settable": True, "unit": "mL"},
        "fraction_alt_media": {"datatype": "float", "settable": True, "unit": "%"},
        "duration": {"datatype": "float", "settable": True, "unit": "min"},
        "array": {"datatype": "array", "settable": True, "unit": "N/A, mL"},
    } 

    def __init__(self, media_ml: float, fraction_alt_media: float, array, **kwargs):
        super(ChemostatAltMedia, self).__init__(**kwargs)

        self.volume = float(media_ml)
        self.counter = 0
        self.index = 0
        self.alt_media_ml = 0
        self.media_ml = float(media_ml)
        alt_dosing_schedule = array 

    def check_for_update(self):
        if self.counter >= alt_dosing_schedule[0][self.index]: # This checks if counter has reached the next scheduled update
            
            # This code should loop the index counter back to zero when it reaches the end of the array
            if self.index >= len(alt_dosing_schedule[0]) - 1:
                self.index = 0 # reset the index counter to zero
                self.counter = 0 # reset the counter back to zero
            else:
                self.index += 1
           # If it is time to update the amount of alternate media, then this updates it
            self.alt_media_ml = alt_dosing_schedule[1][self.index]
 

    def execute(self) -> events.DilutionEvent:
        self.check_for_update()
        self.volume = self.media_ml + self.alt_media_ml
        volume_actually_cycled = self.execute_io_action(alt_media_ml=self.alt_media_ml, media_ml=media_ml, waste_ml=self.volume)
        self.counter += 1
        return events.DilutionEvent(
            f"exchanged {volume_actually_cycled[0]}mL",
            data={"volume_actually_cycled": volume_actually_cycled[0]},
        )
    
if __name__ == "__main__": 
    from pioreactor.background_jobs.dosing_control import DosingController
    
    dc = DosingController(
        "scheduled_chemostat_alt_media",
        duration=20,
        fraction_alt_media=0,
        media_ml = 1.0,
        volume=1.0,
        array=[[5, 10, 20, 40, 60, 80], [0.5, 0, 1.0, 0, 1.5, 0]],
        unit="test_unit",
        experiment="test_experiment"
        )
    dc.block_until_disconnected()

Log File of Error

pioreactor@pioreactor1:~ $ python chemostat_alt_media_2.py
2023-01-04T22:35:53+0000 DEBUG [app] [dosing_control] Init.
2023-01-04T22:35:53+0000 INFO [app] [dosing_control] Starting scheduled_chemostat_alt_media(duration=20, fraction_alt_media=0, media_ml=1.0, volume=1.0, array=[[5, 10, 20, 40, 60, 80], [0.5, 0, 1.0, 0, 1.5, 0]]).
2023-01-04T22:35:54+0000 DEBUG [app] [dosing_automation] Init.
2023-01-04T22:35:55+0000 DEBUG [app] [dosing_automation] Ready.
2023-01-04T22:35:55+0000 INFO [app] [dosing_control] Ready.
2023-01-04T22:35:55+0000 DEBUG [app] [dosing_control] dosing_control is blocking until disconnected.
2023-01-04T22:35:57+0000 DEBUG [app] [dosing_automation] name ‘alt_dosing_schedule’ is not defined
Traceback (most recent call last):
File “/usr/local/lib/python3.9/dist-packages/pioreactor/automations/dosing/base.py”, line 255, in run
event = self.execute()
File “/home/pioreactor/chemostat_alt_media_2.py”, line 56, in execute
self.check_for_update()
File “/home/pioreactor/chemostat_alt_media_2.py”, line 42, in check_for_update
if self.counter >= alt_dosing_schedule[0][self.index]: # This checks if counter has reached the next scheduled update
NameError: name ‘alt_dosing_schedule’ is not defined
2023-01-04T22:35:57+0000 ERROR [app] [dosing_automation] name ‘alt_dosing_schedule’ is not defined
2023-01-04T22:35:57+0000 INFO [app] [dosing_automation] ErrorOccurred: name ‘alt_dosing_schedule’ is not defined

:wave: you’re pretty close. You pass in the array, but then you need to assign it to a class variable (the self part):

   def __init__( ... )
        ...
        self.alt_dosing_schedule = array # note the self.
        ...

After this change, there’s still a problem. You need to also change

self.execute_io_action(..., media_ml=media_ml, ...)

to

self.execute_io_action(..., media_ml=self.media_ml, ...) # self. again

Overall, your code looks good. I have another suggestion, too:

You have duration set to 20min (duration=20). So execute is called every 20min. Hence self.counter is updated every 20min. This means that your array value of 5, 10, etc. really represents 20 * 5 min, 20 * 10min, etc.

It may make more sense for your application to set duration equal to 1 minute (duration=1). That way, your array values have more intuitive meanings.

When I’m testing dosing automation code like you are, I may even drop durations to 0.5, so I can see results faster.

1 Like