Skip to content

Commit

Permalink
introduce new temp config change, fix stirring not unpausing correctl…
Browse files Browse the repository at this point in the history
…y when dodging'
  • Loading branch information
CamDavidsonPilon committed Nov 26, 2024
1 parent f843ae8 commit cd98cd8
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 76 deletions.
4 changes: 4 additions & 0 deletions config.dev.ini
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ target_rpm=500
initial_duty_cycle=15
pwm_hz=200
use_rpm=True
duration_between_updates_seconds=23
post_delay_duration=0.25
pre_delay_duration=2.0
enable_dodging_od=True

[stirring.pid]
Kp=10.0
Expand Down
20 changes: 9 additions & 11 deletions pioreactor/actions/od_blank.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pioreactor import types as pt
from pioreactor import whoami
from pioreactor.config import config
from pioreactor.config import temporary_config_change
from pioreactor.logging import create_logger
from pioreactor.pubsub import prune_retained_messages
from pioreactor.utils import is_pio_job_running
Expand All @@ -26,10 +27,6 @@
from pioreactor.utils.timing import current_utc_datetime


# turn off dodging in the stirring, if set to true.
config["stirring.config"]["enable_dodging_od"] = "False"


def od_statistics(
od_stream: Iterator,
action_name: str,
Expand Down Expand Up @@ -257,13 +254,14 @@ def click_od_blank(ctx, od_angle_channel1, od_angle_channel2, n_samples: int) ->
experiment = whoami.get_assigned_experiment_name(unit)

if ctx.invoked_subcommand is None:
od_blank(
od_angle_channel1,
od_angle_channel2,
n_samples=n_samples,
unit=unit,
experiment=experiment,
)
with temporary_config_change(config, "stirring.config", "enable_dodging_od", "false"):
od_blank(
od_angle_channel1,
od_angle_channel2,
n_samples=n_samples,
unit=unit,
experiment=experiment,
)


@click_od_blank.command(name="delete")
Expand Down
9 changes: 4 additions & 5 deletions pioreactor/actions/self_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from pioreactor.background_jobs.od_reading import REF_keyword
from pioreactor.background_jobs.od_reading import start_od_reading
from pioreactor.config import config
from pioreactor.config import temporary_config_change
from pioreactor.hardware import is_HAT_present
from pioreactor.hardware import is_heating_pcb_present
from pioreactor.hardware import voltage_in_aux
Expand All @@ -57,10 +58,6 @@
from pioreactor.whoami import is_testing_env


# turn off dodging in the stirring, if set to true.
config["stirring.config"]["enable_dodging_od"] = "False"


def test_pioreactor_HAT_present(managed_state, logger: CustomLogger, unit: str, experiment: str) -> None:
assert is_HAT_present(), "HAT is not connected"

Expand Down Expand Up @@ -518,7 +515,9 @@ def click_self_test(k: Optional[str], retry_failed: bool) -> int:
test_positive_correlation_between_rpm_and_stirring,
)

with managed_lifecycle(unit, experiment, "self_test") as managed_state:
with managed_lifecycle(unit, experiment, "self_test") as managed_state, temporary_config_change(
config, "stirring.config", "enable_dodging_od", "false"
):
if any(
is_pio_job_running(
["od_reading", "temperature_automation", "stirring", "dosing_automation", "led_automation"]
Expand Down
1 change: 0 additions & 1 deletion pioreactor/background_jobs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,6 @@ def action_to_do_after_od_reading(self):

def __init__(self, *args, source="app", **kwargs) -> None:
super().__init__(*args, source=source, **kwargs) # type: ignore

self.add_to_published_settings("enable_dodging_od", {"datatype": "boolean", "settable": True})
self.set_enable_dodging_od(self.get_from_config("enable_dodging_od", cast=bool, fallback="True"))

Expand Down
10 changes: 8 additions & 2 deletions pioreactor/background_jobs/stirring.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,9 @@ def action_to_do_before_od_reading(self):
self.on_ready_to_sleeping()

def action_to_do_after_od_reading(self):
self.on_sleeping_to_ready()
self.duty_cycle = self._estimate_duty_cycle
self.rpm_check_repeated_thread.unpause()
self.start_stirring()

def initialize_rpm_to_dc_lookup(self) -> Callable:
if self.rpm_calculator is None:
Expand Down Expand Up @@ -314,6 +316,7 @@ def initialize_rpm_to_dc_lookup(self) -> Callable:
return lambda rpm: self._estimate_duty_cycle

def on_disconnected(self) -> None:
super().on_disconnected()
with suppress(AttributeError):
self.rpm_check_repeated_thread.cancel()
with suppress(AttributeError):
Expand Down Expand Up @@ -423,9 +426,12 @@ def on_ready_to_sleeping(self) -> None:
self.set_duty_cycle(0.0)

def on_sleeping_to_ready(self) -> None:
super().on_sleeping_to_ready()
self.duty_cycle = self._estimate_duty_cycle
self.rpm_check_repeated_thread.unpause()
self.start_stirring()
if not self.enable_dodging_od:
# if we aren't dodging OD readings, we can start stirring right away.
self.start_stirring()

def set_duty_cycle(self, value: float) -> None:
with self.duty_cycle_lock:
Expand Down
35 changes: 33 additions & 2 deletions pioreactor/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import configparser
import os
from contextlib import contextmanager
from functools import cache
from pathlib import Path

Expand Down Expand Up @@ -126,7 +127,7 @@ def get_config() -> ConfigParserMod:
from pioreactor.whoami import is_testing_env

if is_testing_env():
global_config_path = os.environ.get("GLOBAL_CONFIG", "./config.dev.ini")
global_config_path = os.environ.get("GLOBAL_CONFIG", "./.pioreactor/config.dev.ini")
local_config_path = os.environ.get("LOCAL_CONFIG", "")
else:
global_config_path = "/home/pioreactor/.pioreactor/config.ini"
Expand All @@ -136,7 +137,6 @@ def get_config() -> ConfigParserMod:
raise FileNotFoundError(
f"Configuration file at {global_config_path} is missing. Has it completed initializing? Does it need to connect to a leader? Alternatively, use the env variable GLOBAL_CONFIG to specify its location."
)

config_files = [global_config_path, local_config_path]

try:
Expand Down Expand Up @@ -189,3 +189,34 @@ def get_leader_address() -> str:
@cache
def get_mqtt_address() -> str:
return get_config().get("mqtt", "broker_address", fallback=get_leader_address())


@contextmanager
def temporary_config_change(config: ConfigParserMod, section: str, parameter: str, new_value: str):
"""
A context manager to temporarily change a value in a ConfigParser object.
Args:
config (ConfigParser): The ConfigParser instance.
section (str): The section in the ConfigParser to modify.
parameter (str): The parameter in the section to modify.
new_value (str): The temporary value for the parameter.
Yields:
None: The context where the parameter is temporarily modified.
"""
if not config.has_section(section):
raise ValueError(f"Section '{section}' does not exist in the configuration.")
if not config.has_option(section, parameter):
raise ValueError(f"Parameter '{parameter}' does not exist in section '{section}'.")

# Save the original value
original_value = config.get(section, parameter)

try:
# Apply the temporary change
config.set(section, parameter, new_value)
yield
finally:
# Restore the original value
config.set(section, parameter, original_value)
122 changes: 67 additions & 55 deletions pioreactor/tests/test_od_reading.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
from pioreactor import structs
from pioreactor.background_jobs.od_reading import ADCReader
from pioreactor.background_jobs.od_reading import CachedCalibrationTransformer
from pioreactor.background_jobs.od_reading import closest_point_to_domain
from pioreactor.background_jobs.od_reading import NullCalibrationTransformer
from pioreactor.background_jobs.od_reading import ODReader
from pioreactor.background_jobs.od_reading import PhotodiodeIrLedReferenceTrackerStaticInit
from pioreactor.background_jobs.od_reading import start_od_reading
from pioreactor.config import config
from pioreactor.config import temporary_config_change
from pioreactor.pubsub import collect_all_logs_of_level
from pioreactor.utils import local_persistant_storage
from pioreactor.utils.timing import current_utc_datetime
Expand Down Expand Up @@ -796,37 +798,34 @@ def test_calibration_multi_modal() -> None:
def test_calibration_errors_when_ir_led_differs() -> None:
experiment = "test_calibration_errors_when_ir_led_differs"

config["od_reading.config"]["ir_led_intensity"] = "90"

with local_persistant_storage("current_od_calibration") as c:
c["90"] = encode(
structs.OD90Calibration(
created_at=current_utc_datetime(),
curve_type="poly",
curve_data_=[1.0, 0, -0.1],
name="quad_test",
maximum_od600=2.0,
minimum_od600=0.0,
ir_led_intensity=50.0,
angle="90",
minimum_voltage=0.0,
maximum_voltage=1.0,
voltages=[],
od600s=[],
pd_channel="2",
pioreactor_unit=get_unit_name(),
with temporary_config_change(config, "od_reading.config", "ir_led_intensity", "90"):
with local_persistant_storage("current_od_calibration") as c:
c["90"] = encode(
structs.OD90Calibration(
created_at=current_utc_datetime(),
curve_type="poly",
curve_data_=[1.0, 0, -0.1],
name="quad_test",
maximum_od600=2.0,
minimum_od600=0.0,
ir_led_intensity=50.0,
angle="90",
minimum_voltage=0.0,
maximum_voltage=1.0,
voltages=[],
od600s=[],
pd_channel="2",
pioreactor_unit=get_unit_name(),
)
)
)

with pytest.raises(exc.CalibrationError) as error:
with start_od_reading("REF", "90", interval=1, fake_data=True, experiment=experiment):
pass
assert "LED intensity" in str(error.value)

with local_persistant_storage("current_od_calibration") as c:
del c["90"]
with pytest.raises(exc.CalibrationError) as error:
with start_od_reading("REF", "90", interval=1, fake_data=True, experiment=experiment):
pass
assert "LED intensity" in str(error.value)

config["od_reading.config"]["ir_led_intensity"] = "auto"
with local_persistant_storage("current_od_calibration") as c:
del c["90"]


def test_calibration_errors_when_pd_channel_differs() -> None:
Expand Down Expand Up @@ -1048,42 +1047,27 @@ def test_calibration_data_from_user2() -> None:


def test_auto_ir_led_intensit_REF_and_90() -> None:
existing_intensity = config["od_reading.config"]["ir_led_intensity"]
with temporary_config_change(config, "od_reading.config", "ir_led_intensity", "auto"):
experiment = "test_auto_ir_led_intensity"

config["od_reading.config"]["ir_led_intensity"] = "auto"

experiment = "test_auto_ir_led_intensity"

with start_od_reading("REF", "90", interval=None, fake_data=True, experiment=experiment) as od:
assert abs(od.ir_led_intensity - 67.19794921875) < 0.01

config["od_reading.config"]["ir_led_intensity"] = existing_intensity
with start_od_reading("REF", "90", interval=None, fake_data=True, experiment=experiment) as od:
assert abs(od.ir_led_intensity - 67.19794921875) < 0.01


def test_auto_ir_led_intensity_90_only() -> None:
existing_intensity = config["od_reading.config"]["ir_led_intensity"]

config["od_reading.config"]["ir_led_intensity"] = "auto"

experiment = "test_auto_ir_led_intensity"
with temporary_config_change(config, "od_reading.config", "ir_led_intensity", "auto"):
experiment = "test_auto_ir_led_intensity"

with start_od_reading(None, "90", interval=None, fake_data=True, experiment=experiment) as od:
assert od.ir_led_intensity == 70.0

config["od_reading.config"]["ir_led_intensity"] = existing_intensity
with start_od_reading(None, "90", interval=None, fake_data=True, experiment=experiment) as od:
assert od.ir_led_intensity == 70.0


def test_auto_ir_led_intensity_90_and_90() -> None:
existing_intensity = config["od_reading.config"]["ir_led_intensity"]

config["od_reading.config"]["ir_led_intensity"] = "auto"

experiment = "test_auto_ir_led_intensity"
with temporary_config_change(config, "od_reading.config", "ir_led_intensity", "auto"):
experiment = "test_auto_ir_led_intensity"

with start_od_reading("90", "90", interval=None, fake_data=True, experiment=experiment) as od:
assert od.ir_led_intensity == 70.0

config["od_reading.config"]["ir_led_intensity"] = existing_intensity
with start_od_reading("90", "90", interval=None, fake_data=True, experiment=experiment) as od:
assert od.ir_led_intensity == 70.0


def test_at_least_one_channel() -> None:
Expand Down Expand Up @@ -1214,3 +1198,31 @@ def test_CachedCalibrationTransformer_with_real_calibration() -> None:
cal_transformer.hydate_models_from_disk({"2": "90"})

assert abs(cal_transformer({"2": 0.096})["2"] - 0.06) < 0.01


def test_closest_point_single_point_in_domain():
assert closest_point_to_domain([1.0], (0.5, 1.5)) == 1.0


def test_closest_point_multiple_points_in_domain():
assert closest_point_to_domain([0.6, 0.8, 1.2], (0.5, 1.5)) == 0.6


def test_closest_point_all_outside_domain():
assert closest_point_to_domain([2.0, 3.0], (0.5, 1.5)) == 2.0
assert closest_point_to_domain([-1.0, -2.0], (0.5, 1.5)) == -1.0


def test_closest_point_empty_list():
with pytest.raises(AssertionError):
closest_point_to_domain([], (0.5, 1.5))


def test_closest_point_on_boundaries():
assert closest_point_to_domain([0.5, 1.5], (0.5, 1.5)) == 0.5


def test_missing_calibration_data():
cal_transformer = CachedCalibrationTransformer()
cal_transformer.models = {"1": lambda v: v * 2}
assert cal_transformer({"1": 1.0, "2": 2.0}) == {"1": 2.0, "2": 2.0}

0 comments on commit cd98cd8

Please sign in to comment.