Using the Pioreactor for algae growth

I’ve ordered some algae, Chlorella vulgaris, and BBM media from University of Waterloo. I plan to test out how the Pioreactor can handle growing algae. Should arrive in a few weeks.

What’s needed?

  1. Some white LED lights. I’m going to use white lights (instead of red/blue) to keep things simple for now.
  2. A script for controlling the LEDs - I could have the LEDs on at all times, but this isn’t good for algae growth. They prefer an on/off schedaule. Also, I want to see if the growth can be modulated by on/off cycles, and that the Pioreactor can detect it.

Open questions:

  1. Do I need a bubble, or will stirring be enough?
  2. What’s a good RPM - seeing online that ~100RPM is good. Can the Pioreactor go that low?
2 Likes

I’ve written up a new, simple, LED automation for Light/Dark cycles. Let’s go through it. The basic idea is that the LED automation “wakes up” (specfically: it runs its execute method) every hour, and on specific hours, turns on or turns off the white light LEDs.

The Code

  1. Imports at the top of the file!
# -*- coding: utf-8 -*-
import signal
from pioreactor.automations import LEDAutomationContrib
from pioreactor.whoami import get_unit_name, get_latest_experiment_name
from pioreactor.automations import events
  1. Define a new class as a subclass of LEDAutomationContrib. We use LEDAutomationContrib since this is a 3rd party automation. We give the new class a descriptive name. The key attribute is necessary - and is often just the camel case version of the class name.
class LightDarkCycle(LEDAutomationContrib):
    key = "light_dark_cycle"
  1. Define our published_settings for this class. This is a dictionary of LightDarkCycle attributes that we can modify/inspect from MQTT (and hence from the UI, or from the leader Pioreactor). Not all attributes need to go in here - only the ones that users may want to modify mid-experiment.

Later in this post, we’ll see what each of these attributes does.

    published_settings = {
        "duration": {"datatype": "float", "settable": False, "unit": "min"},
        "light_intensity": {"datatype": "float", "settable": True, "unit": "%"},
        "light_duration_hours": {"datatype": "float", "settable": True, "unit": "h"},
        "dark_duration_hours": {"datatype": "float", "settable": True, "unit": "h"},
    }
  1. Create the __init__, with the attributes we’ll need.
    def __init__(self, light_intensity: float, light_duration_hours: int, dark_duration_hours: int, **kwargs):
        super().__init__(**kwargs)
        self.hours_online: int = -1
        self.light_active: bool = False
        self.channels = ["B", "C"]
        self.set_light_intensity(light_intensity)
        self.light_duration_hours = float(light_duration_hours)
        self.dark_duration_hours = float(dark_duration_hours)
  • hours_online will keep track of how many elapsed hours have gone by.
  • light_active keeps track of whether the LEDs are currently on or off
  • channels refers to which LED channels we’ll use for white light.
  • light_duration_hours: the number of hours to keep the light on for, typically 16
  • dark_duration_hours: the number of hours to keep the light off for, typically 8
  • light_intensity: the level of intensity, as a percent, of the LEDs when turned on.
  1. We define the execute function, which is what runs every duration minutes. In the function, we increment hours_online by 1 (since it runs every 60 minutes), and ask a separate function, trigger_leds, to handle turning on and off LEDs. The execute should return an Event.
    def execute(self):
        self.hours_online += 1
        event = self.trigger_leds(self.hours_online)
        return event

The other class function, trigger_leds, role is to:

  1. determine if we should turn on LEDs, turn off LEDs, or do nothing.
  2. If we are changing LEDs status (on to off, or off to on), perform that task.
  3. return an Event, with some description of what occurred (even if nothing changed).

To do 1., we think about the total hours passed modulo how long our cycle is. That is, if our cycle lasts 24 hours (which might be the result of choosing 16h light + 8h dark), then the hour 33 is really the same as hour 9, likewise the hour 123 is the same as hour 3: we take the hour modulo the duration.

We then ask, is this hour in the dark period, or the light period, of the cycle? We also ask if the lights_active is on, or off, respectively? If so, we change the status of the LEDs. For example, if we should be in the dark period, but our LEDs are on, well, we turn them off, and return a ChangeLEDIntensity event. The function set_led_intensity is from the parent class, and is a helper function.

    def trigger_leds(self, hours: int) -> events.Event:
        cycle_duration: int = self.light_duration_hours + self.dark_duration_hours

        if ((hours % cycle_duration) < self.light_duration_hours) and (not self.light_active):
            self.light_active = True
            for channel in self.channels:
                self.set_led_intensity(channel, self.light_intensity)
            return events.ChangedLedIntensity(f"{hours}h: turned on LEDs")
        elif ((hours % cycle_duration) >= self.light_duration_hours) and (self.light_active):
            self.light_active = False
            for channel in self.channels:
                self.set_led_intensity(channel, 0)
            return events.ChangedLedIntensity(f"{hours}h: turned off LEDs")
        else:
            return events.NoEvent(f"{hours}h: no change")

We also need to define that set_light_intensity function above. This function is automatically called whenever we change light_intensity. We need additional logic to immediately change the light_intensity when asked (otherwise, the LEDs wouldn’t actually update until the next execute is called).

    def set_light_intensity(self, intensity):
        self.light_intensity = float(intensity)
        if self.light_active:
            # update now!
            for channel in self.channels:
                self.set_led_intensity(channel, self.light_intensity)
        else:
            pass

That’s the end of the class! We turn it into a runnable script with the following, at the end of the file:

if __name__ == "__main__":
    from pioreactor.background_jobs.led_control import LEDController
    from pioreactor.whoami import get_unit_name, get_latest_experiment_name

    lc = LEDController(
        led_automation="light_dark_cycle",
        light_intensity=45.0,
        light_duration_hours=16,
        dark_duration_hours=8,
        duration=60, # every 60min we "wake up" and decide what to do.
        skip_first_run=False,
        unit=get_unit_name(),
        experiment=get_latest_experiment_name()
    )

    lc.block_until_disconnected()

In total, our file looks like:

# -*- coding: utf-8 -*-
from __future__ import annotations
import signal
from pioreactor.automations import LEDAutomationContrib
from pioreactor.automations import events
from pioreactor.actions.led_intensity import LED_Channel

__plugin_summary__ = "An LED automation for ON/OFF cycles"
__plugin_version__ = "0.0.2"
__plugin_name__ = "Light Dark Cycle LED automation"

class LightDarkCycle(LEDAutomationContrib):
    """
    Follows as h light / h dark cycle. Starts dark.
    """

    key: str = "light_dark_cycle"
    published_settings: dict[str, dict] = {
        "duration": {"datatype": "float", "settable": False, "unit": "min"}, # doesn't make sense to change duration.
        "light_intensity": {"datatype": "float", "settable": True, "unit": "%"},
        "light_duration_hours": {"datatype": "int", "settable": True, "unit": "h"},
        "dark_duration_hours": {"datatype": "int", "settable": True, "unit": "h"},
    }

    def __init__(self, light_intensity: float, light_duration_hours: int, dark_duration_hours: int, **kwargs):
        super().__init__(**kwargs)
        self.hours_online: int = -1
        self.light_active: bool = False
        self.channels: list[LED_Channel] = [LED_Channel("B"), LED_Channel("C")]
        self.set_light_intensity(light_intensity)
        self.light_duration_hours = float(light_duration_hours)
        self.dark_duration_hours = float(dark_duration_hours)


    def trigger_leds(self, hours: int) -> events.Event:
        cycle_duration: int = self.light_duration_hours + self.dark_duration_hours

        if ((hours % cycle_duration) < self.light_duration_hours) and (not self.light_active):
            self.light_active = True
            for channel in self.channels:
                self.set_led_intensity(channel, self.light_intensity)
            return events.ChangedLedIntensity(f"{hours}h: turned on LEDs")
        elif ((hours % cycle_duration) >= self.light_duration_hours) and (self.light_active):
            self.light_active = False
            for channel in self.channels:
                self.set_led_intensity(channel, 0)
            return events.ChangedLedIntensity(f"{hours}h: turned off LEDs")
        else:
            return events.NoEvent(f"{hours}h: no change")


    def set_light_intensity(self, intensity):
        self.light_intensity = float(intensity)
        if self.light_active:
            # update now!
            for channel in self.channels:
                self.set_led_intensity(channel, self.light_intensity)
        else:
            pass


    def execute(self) -> events.Event:
        self.hours_online += 1
        event = self.trigger_leds(self.hours_online)
        return event



if __name__ == "__main__":
    from pioreactor.background_jobs.led_control import LEDController
    from pioreactor.whoami import get_unit_name, get_latest_experiment_name

    lc = LEDController(
        led_automation="light_dark_cycle",
        light_intensity=45.0,
        light_duration_hours=16,
        dark_duration_hours=8,
        duration=60, # every 60min we "wake up" and decide what to do.
        skip_first_run=False,
        unit=get_unit_name(),
        experiment=get_latest_experiment_name()
    )

    lc.block_until_disconnected()

Setting up the Pioreactor

Setting up your Pioreactor is easy: attach white LEDs to LED channels B and C, and stick them in pockets X2 and X3.

Running the automation

Let’s save this file to our Pioreactor, by accessing the Pioreactor’s command line, typing nano light_dark_cycle.py, and pasting in the code above.

You can test the automation from the Pioreactor’s command line by executing (you may want to change the duration to something like 0.5, so you’re not waiting hours to see it change):

python3 light_dark_cycle.py

A further extension the above algorithm, and one that is easier to code and more realistic, is an algorithm that interpolates between the light and darkness. I suspect 16h light / 8hr dark is common because it’s easy to program into timed-switches.

Overall, the project was a success! We successfully grew algae:

2 Likes

Updated code:

# -*- coding: utf-8 -*-
from __future__ import annotations
import signal
from pioreactor.automations import LEDAutomationJobContrib
from pioreactor.automations import events
from pioreactor.types import LedChannel
from math import exp

__plugin_summary__ = "An LED automation for smooth light cycles"
__plugin_version__ = "0.0.2"
__plugin_name__ = "Light Cycle LED automation"
__plugin_author__ = "Cameron Davidson-Pilon"
__plugin_homepage__ = "https://github.com/pioreactor"


def logistic(t, k, d):
    return 1 / (1 + exp(-k * (t - d)))


def light_at_time_t(t):
    t = t % 24
    if t < 16:
        return logistic(t, 1.5, 5)
    else:
        return logistic(t, -1.5, 5 + 16)


class LightCycle(LEDAutomationJobContrib):

    automation_name: str = "light_cycle"
    published_settings: dict[str, dict] = {
        "duration": {
            "datatype": "float",
            "settable": False,
            "unit": "min",
        },  # doesn't make sense to change duration.
        "max_light_intensity": {"datatype": "float", "settable": True, "unit": "%"},
    }

    def __init__(self, max_light_intensity: float, **kwargs):
        super().__init__(**kwargs)
        self.hours_online: int = -1
        self.channels: list[LedChannel] = ["B", "C"]
        self.max_light_intensity = float(max_light_intensity)

    def execute(self) -> events.AutomationEvent:
        self.hours_online += 1
        new_intensity = self.max_light_intensity * light_at_time_t(self.hours_online)
        for channel in self.channels:
            self.set_led_intensity(channel, new_intensity)
        return events.ChangedLedIntensity(f"Changed intensity to {new_intensity:0.2f}%")


if __name__ == "__main__":
    from pioreactor.background_jobs.led_control import LEDController
    from pioreactor.whoami import get_unit_name, get_latest_experiment_name

    lc = LEDController(
        led_automation="light_cycle",
        max_light_intensity=2.0,
        duration=0.03,  # every Xmin we "wake up" and decide what to do.
        unit=get_unit_name(),
        experiment=get_latest_experiment_name(),
    )

    lc.block_until_disconnected()

3 Likes