Skip to content

Commit 03ed7ce

Browse files
align it with the existing light/dark cycle automation
1 parent b0f3713 commit 03ed7ce

File tree

3 files changed

+73
-87
lines changed

3 files changed

+73
-87
lines changed

README.md

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ This plugin is also available on the Pioreactor web interface, in the _Plugins_
2828
Type into your command line:
2929

3030
```
31-
pio run led_calibration
31+
pio calibrations --device-name led_C --protocol-name led_calibration
32+
pio calibrations --device-name led_D --protocol-name led_calibration
3233
```
3334

3435
To perform this calibration, insert your vial containing media into the Pioreactor and submerge your light probe. Follow the prompts on the command line. The plugin will increase the light intensity, and prompt you to record the readings from your light probe. A calibration line of best fit will be generated based on your light probe readings.
@@ -39,32 +40,6 @@ An automation will become available on the web interface. To use this automation
3940

4041
In the _Pioreactors_ tab, under _Manage_, you can _Start_ an _LED automation_. A new option becomes available in the drop-down menu called "Calibrated Light/Dark Cycle". Input your desired light intensity in AU (ex. 1000 AU). The automation will set the percent light intensity such that an output of 1000 AU occurs on **both** LEDs.
4142

42-
## Subcommands
43-
44-
Run a subcommand by typing the following into the command line:
45-
```
46-
pio run led_calibration <SUBCOMMAND>
47-
```
48-
The following subcommands are available:
49-
50-
### **list**
51-
Prints a table with all existing calibrations stored on the leader. Headings include unique names, timestamps, and channels.
52-
53-
| Name | Timestamp | Channel | Currently in use? |
54-
|------|----------|---------|-------------------|
55-
| Algae_C_2022 | 2022-08-29T20:12:00.400000Z | C ||
56-
| Algae_B_2022 | 2022-08-29T20:13:00.400000Z | B ||
57-
| Algae_B_2021 | 2021-08-29T20:15:00.400000Z | B | |
58-
59-
### **display**
60-
Displays the graph and data for the current calibration for each channel A, B, C, and D, if it exists. For example, for the data above, the current calibrations for Algae_C_2022 and Algae_B_2022 will be displayed.
61-
62-
### **change_current**
63-
If you would like to change a current calibration to a previous one, use `change_current "<UNIQUE NAME>"`. These changes are based on the channel assigned to the calibration.
64-
65-
For example:
66-
`pio run led_calibration change_current "Algae_B_2021"` would replace Algae_B_2022, since only one calibation is active per channel.
67-
6843
## When to perform an LED calibration
6944

7045
Calibrations should be performed on a case-by-case basis. A new calibration must be performed per channel, and/or for new LED cables, and with any change in media that can alter the light intensity within the vial.
Lines changed: 55 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,48 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import annotations
33

4-
from msgspec.json import decode
4+
from typing import Optional
5+
56
from pioreactor.automations import events
67
from pioreactor.automations.led.base import LEDAutomationJobContrib
8+
from pioreactor.calibrations import load_active_calibration
79
from pioreactor.exc import CalibrationError
810
from pioreactor.types import LedChannel
911
from pioreactor.utils import clamp
1012
from pioreactor.utils import local_persistent_storage
1113

12-
from .led_calibration import LEDCalibration
13-
1414

1515
class CalibratedLightDarkCycle(LEDAutomationJobContrib):
1616
"""
17-
Follows as h light / h dark cycle. Starts light ON.
17+
Follows as min light / min dark cycle. Starts light ON.
1818
"""
1919

20-
automation_name = "calibrated_light_dark_cycle"
20+
automation_name: str = "calibrated_light_dark_cycle"
2121
published_settings = {
2222
"duration": {
2323
"datatype": "float",
2424
"settable": False,
2525
"unit": "min",
2626
},
27-
"light_intensity": {"datatype": "float", "settable": True, "unit": "AU"},
28-
"light_duration_hours": {"datatype": "integer", "settable": True, "unit": "h"},
29-
"dark_duration_hours": {"datatype": "integer", "settable": True, "unit": "h"},
27+
"light_intensity": {"datatype": "float", "settable": True, "unit": "%"},
28+
"light_duration_minutes": {"datatype": "integer", "settable": True, "unit": "min"},
29+
"dark_duration_minutes": {"datatype": "integer", "settable": True, "unit": "min"},
3030
}
3131

3232
def __init__(
3333
self,
34-
light_intensity: float,
35-
light_duration_hours: int,
36-
dark_duration_hours: int,
34+
light_intensity: float | str,
35+
light_duration_minutes: int | str,
36+
dark_duration_minutes: int | str,
3737
**kwargs,
3838
):
3939
super().__init__(**kwargs)
40-
self.hours_online: int = -1
40+
self.minutes_online: int = -1
4141
self.light_active: bool = False
42-
self.channels: list[LedChannel] = ["C", "D"]
42+
self.channels: list[LedChannel] = ["D", "C"]
4343
self.set_light_intensity(light_intensity)
44-
self.light_duration_hours = float(light_duration_hours)
45-
self.dark_duration_hours = float(dark_duration_hours)
44+
self.light_duration_minutes = float(light_duration_minutes)
45+
self.dark_duration_minutes = float(dark_duration_minutes)
4646

4747
with local_persistent_storage("active_calibrations") as cache:
4848
for channel in self.channels:
@@ -52,55 +52,66 @@ def __init__(
5252
def channel_to_led_device(self, channel: LedChannel) -> str:
5353
return f"led_{channel}"
5454

55-
def execute(self) -> events.AutomationEvent:
56-
self.hours_online += 1
57-
return self.trigger_leds(self.hours_online)
55+
def execute(self) -> Optional[events.AutomationEvent]:
56+
# runs every minute
57+
self.minutes_online += 1
58+
return self.trigger_leds(self.minutes_online)
59+
60+
def calculate_intensity_percent(self, channel: str) -> float:
5861

59-
def calculate_intensity_percent(self, channel):
60-
with local_persistent_storage("current_led_calibration") as cache:
61-
led_calibration = decode(
62-
cache[self.channel_to_led_device(channel)], type=LEDCalibration
63-
)
62+
led_calibration = load_active_calibration(self.channel_to_led_device(channel))
6463

65-
intensity_percent = (
66-
self.light_intensity - led_calibration.curve_data_[1]
67-
) / led_calibration.curve_data_[0]
64+
intensity_percent = led_calibration.ipredict(self.light_intensity)
6865

69-
return clamp(0, intensity_percent, 100)
66+
return clamp(0, intensity_percent, 100)
7067

71-
def trigger_leds(self, hours: int) -> events.AutomationEvent:
68+
def trigger_leds(self, minutes: int) -> Optional[events.AutomationEvent]:
69+
"""
70+
Changes the LED state based on the current minute in the cycle.
7271
73-
cycle_duration = self.light_duration_hours + self.dark_duration_hours
72+
The light and dark periods are calculated as multiples of 60 minutes, forming a cycle.
73+
Based on where in this cycle the current minute falls, the light is either turned ON or OFF.
7474
75-
if ((hours % cycle_duration) < self.light_duration_hours) and (not self.light_active):
75+
Args:
76+
minutes: The current minute of the cycle.
77+
78+
Returns:
79+
An instance of AutomationEvent, indicating that LEDs' status might have changed.
80+
Returns None if the LEDs' state didn't change.
81+
"""
82+
cycle_duration_min = int(self.light_duration_minutes + self.dark_duration_minutes)
83+
if ((minutes % cycle_duration_min) < (self.light_duration_minutes)) and (
84+
not self.light_active
85+
):
7686
self.light_active = True
7787

7888
for channel in self.channels:
7989
intensity_percent = self.calculate_intensity_percent(channel)
8090
self.set_led_intensity(channel, intensity_percent)
8191

82-
return events.ChangedLedIntensity(f"{hours}h: turned on LEDs.")
92+
return events.ChangedLedIntensity(f"{minutes:.1f}min: turned on LEDs.")
8393

84-
elif ((hours % cycle_duration) >= self.light_duration_hours) and (self.light_active):
94+
elif ((minutes % cycle_duration_min) >= (self.light_duration_minutes)) and (
95+
self.light_active
96+
):
8597
self.light_active = False
8698
for channel in self.channels:
8799
self.set_led_intensity(channel, 0)
88-
return events.ChangedLedIntensity(f"{hours}h: turned off LEDs.")
100+
return events.ChangedLedIntensity(f"{minutes:.1f}min: turned off LEDs.")
89101

90102
else:
91-
return events.NoEvent(f"{hours}h: no change.")
92-
93-
def set_dark_duration_hours(self, hours: int):
94-
95-
self.dark_duration_hours = hours
103+
return None
96104

97-
self.trigger_leds(self.hours_online)
105+
# minutes setters
106+
def set_dark_duration_minutes(self, minutes: int):
107+
self.dark_duration_minutes = minutes
98108

99-
def set_light_duration_hours(self, hours: int):
109+
self.trigger_leds(self.minutes_online)
100110

101-
self.light_duration_hours = hours
111+
def set_light_duration_minutes(self, minutes: int):
112+
self.light_duration_minutes = minutes
102113

103-
self.trigger_leds(self.hours_online)
114+
self.trigger_leds(self.minutes_online)
104115

105116
def set_light_intensity(self, intensity_au):
106117
# this is the setter of light_intensity attribute, eg. called when updated over MQTT
@@ -116,6 +127,6 @@ def set_light_intensity(self, intensity_au):
116127
pass
117128

118129
def set_duration(self, duration: float) -> None:
119-
if duration != 60:
120-
self.logger.warning("Duration should be set to 60.")
130+
if duration != 1:
131+
self.logger.warning("Duration should be set to 1.")
121132
super().set_duration(duration)

led_calibration_plugin/test_led_calibration.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pioreactor.exc import CalibrationError
88
from pioreactor.utils import local_intermittent_storage
99
from pioreactor.utils import local_persistent_storage
10-
from pioreactor.utils.timing import current_utc_timestamp
10+
from pioreactor.utils.timing import current_utc_datetime
1111
from pioreactor.whoami import get_unit_name
1212

1313
from .calibrated_light_dark_cycle import CalibratedLightDarkCycle
@@ -24,30 +24,30 @@ def test_led_fails_if_calibration_not_present():
2424

2525
with local_persistent_storage("active_calibrations") as cache:
2626
if "led_C" in cache:
27-
del cache["led_C"]
27+
cache.pop("led_C")
2828
if "led_D" in cache:
29-
del cache["led_D"]
29+
cache.pop("led_D")
3030

3131
with pytest.raises(CalibrationError):
3232

3333
with CalibratedLightDarkCycle(
34-
duration=0.01,
34+
duration=1,
3535
light_intensity=-1,
36-
light_duration_hours=16,
37-
dark_duration_hours=8,
36+
light_duration_minutes=16,
37+
dark_duration_minutes=8,
3838
unit=unit,
3939
experiment=experiment,
4040
):
4141

42-
pause(8)
42+
pass
4343

4444

4545
def test_set_intensity_au_above_max() -> None:
4646
experiment = "test_set_intensity_au_above_max"
4747
unit = get_unit_name()
4848

4949
cal = LEDCalibration(
50-
created_at=current_utc_timestamp(),
50+
created_at=current_utc_datetime(),
5151
calibrated_on_pioreactor_unit=unit,
5252
calibration_name=experiment,
5353
curve_data_=[1, 0],
@@ -60,14 +60,14 @@ def test_set_intensity_au_above_max() -> None:
6060
cal.set_as_active_calibration_for_device("led_D")
6161

6262
with CalibratedLightDarkCycle(
63-
duration=0.01,
63+
duration=1,
6464
light_intensity=1500,
65-
light_duration_hours=16,
66-
dark_duration_hours=8,
65+
light_duration_minutes=16,
66+
dark_duration_minutes=8,
6767
unit=unit,
6868
experiment=experiment,
6969
) as lc:
70-
70+
pause(10)
7171
assert lc.light_intensity == 1500 # test returns light_intensity (au)
7272

7373
lc.set_light_intensity(2000)
@@ -80,7 +80,7 @@ def test_set_intensity_au_negative() -> None:
8080
unit = get_unit_name()
8181

8282
cal = LEDCalibration(
83-
created_at=current_utc_timestamp(),
83+
created_at=current_utc_datetime(),
8484
calibrated_on_pioreactor_unit=unit,
8585
calibration_name=experiment,
8686
curve_data_=[1, 0],
@@ -93,10 +93,10 @@ def test_set_intensity_au_negative() -> None:
9393
cal.set_as_active_calibration_for_device("led_D")
9494

9595
with CalibratedLightDarkCycle(
96-
duration=0.01,
96+
duration=1,
9797
light_intensity=-1,
98-
light_duration_hours=16,
99-
dark_duration_hours=8,
98+
light_duration_minutes=16,
99+
dark_duration_minutes=8,
100100
unit=unit,
101101
experiment=experiment,
102102
) as lc:

0 commit comments

Comments
 (0)