Skip to content

Commit

Permalink
stirring -> stirring.config, od_config -> od_reading.config
Browse files Browse the repository at this point in the history
  • Loading branch information
CamDavidsonPilon committed Aug 1, 2024
1 parent 9e2a746 commit 3a8e3ae
Show file tree
Hide file tree
Showing 17 changed files with 124 additions and 65 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
- more resilience to "UI state" diverging from "bioreactor state". Often, this occurred when two jobs stared almost immediately (often a networking issue), and the last job would halt since it couldn't get the required resources, however MQTT any data would be overwritten by the last job. Now, multiple places in the request pipeline will reduce duplication.
- Improved stirring clean up when stopped in quick succession after starting.

#### Breaking changes

- in config.ini, the section `od_config` is now `od_reading.config`, and `stirring` is `stirring.config`. When you update, a script will run to automatically update these names in your config.inis.

### 24.7.18

#### Enhancements
Expand Down
10 changes: 5 additions & 5 deletions pioreactor/actions/od_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def get_name_from_user() -> str:


def get_metadata_from_user() -> tuple[pt.OD600, pt.OD600, pt.mL, pt.PdAngle, pt.PdChannel]:
if config["od_config"]["ir_led_intensity"] == "auto":
if config["od_reading.config"]["ir_led_intensity"] == "auto":
raise ValueError(
"Can't use auto with OD calibrations. Change ir_led_intensity in your config.ini to a numeric value (70 is good default)."
)
Expand Down Expand Up @@ -166,7 +166,7 @@ def start_stirring():
echo("Starting stirring and blocking until near target RPM.")

st = stirring(
target_rpm=config.getfloat("stirring", "target_rpm"),
target_rpm=config.getfloat("stirring.config", "target_rpm"),
unit=get_unit_name(),
experiment=get_testing_experiment_name(),
)
Expand Down Expand Up @@ -476,7 +476,7 @@ def save_results(
curve_type=curve_type,
voltages=voltages,
od600s=od600s,
ir_led_intensity=float(config["od_config"]["ir_led_intensity"]),
ir_led_intensity=float(config["od_reading.config"]["ir_led_intensity"]),
pd_channel=pd_channel,
)

Expand Down Expand Up @@ -565,11 +565,11 @@ def od_calibration(data_file: str | None) -> None:
echo()
echo(f"Finished calibration of `{name}` ✅")

if not config.getboolean("od_config", "use_calibration", fallback=False):
if not config.getboolean("od_reading.config", "use_calibration", fallback=False):
echo()
echo(
bold(
"Currently [od_config][use_calibration] is set to 0 in your config.ini. This should be set to 1 to use calibrations.",
"Currently [od_reading.config][use_calibration] is set to 0 in your config.ini. This should be set to 1 to use calibrations.",
)
)
return
Expand Down
4 changes: 2 additions & 2 deletions pioreactor/actions/self_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def test_REF_is_lower_than_0_dot_256_volts(
) -> None:
reference_channel = cast(PdChannel, config.get("od_config.photodiode_channel_reverse", REF_keyword))
ir_channel = cast(LedChannel, config["leds_reverse"][IR_keyword])
config_ir_intensity = config.get("od_config", "ir_led_intensity")
config_ir_intensity = config.get("od_reading.config", "ir_led_intensity")
if config_ir_intensity == "auto":
ir_intensity = 50.0 # this has been our historical default, and should generally work. Default now is "auto", which targets 0.225 V into REF
else:
Expand Down Expand Up @@ -400,7 +400,7 @@ def test_positive_correlation_between_rpm_and_stirring(
initial_dc = rpm_coef * 700 + intercept

else:
initial_dc = config.getfloat("stirring", "initial_duty_cycle")
initial_dc = config.getfloat("stirring.config", "initial_duty_cycle")

dcs = []
measured_rpms = []
Expand Down
2 changes: 1 addition & 1 deletion pioreactor/actions/stirring_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def click_stirring_calibration(min_dc: int, max_dc: int) -> None:

if max_dc is None and min_dc is None:
# seed with initial_duty_cycle
config_initial_duty_cycle = config.getfloat("stirring", "initial_duty_cycle")
config_initial_duty_cycle = config.getfloat("stirring.config", "initial_duty_cycle")
min_dc, max_dc = round(config_initial_duty_cycle * 0.75), round(config_initial_duty_cycle * 1.33)
elif (max_dc is not None) and (min_dc is not None):
assert min_dc < max_dc, "min_dc >= max_dc"
Expand Down
4 changes: 0 additions & 4 deletions pioreactor/automations/temperature/thermostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,11 @@ def execute(self) -> UpdatedHeaterDC:

def set_target_temperature(self, target_temperature: float | str) -> None:
"""
Parameters
------------
target_temperature: float
the new target temperature
update_dc_now: bool
if possible, update the DC% to approach the new target temperature
"""
target_temperature = float(target_temperature)
self.target_temperature = self._clamp_target_temperature(target_temperature)
Expand Down
10 changes: 3 additions & 7 deletions pioreactor/background_jobs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -973,11 +973,6 @@ def _check_for_duplicate_activity(self) -> None:
if is_pio_job_running(self.job_name) and not is_testing_env():
self.logger.warning(f"{self.job_name} is already running. Skipping")
raise RuntimeError(f"{self.job_name} is already running. Skipping")
# elif is_pio_job_running("self_test"):
# # don't ever run anything while self_test runs.
# self.logger.error("self_test is running.")
# raise RuntimeError("self_test is running.")
### this doesn't work because self_test invokes jobs, and we hit this line.

def __setattr__(self, name: str, value: t.Any) -> None:
super(_BackgroundJob, self).__setattr__(name, value)
Expand Down Expand Up @@ -1149,6 +1144,7 @@ def sneak_in(ads_interval, post_delay, pre_delay) -> None:
if self.state != self.READY:
return

self.is_after_period = True
self.action_to_do_after_od_reading()
sleep(ads_interval - self.OD_READING_DURATION - (post_delay + pre_delay))
self.is_after_period = False
Expand All @@ -1160,13 +1156,13 @@ def sneak_in(ads_interval, post_delay, pre_delay) -> None:
ads_start_time_msg = subscribe(
f"pioreactor/{self.unit}/{self.experiment}/od_reading/first_od_obs_time"
)
if ads_start_time_msg:
if ads_start_time_msg and ads_start_time_msg.payload:
ads_start_time = float(ads_start_time_msg.payload)
else:
return

ads_interval_msg = subscribe(f"pioreactor/{self.unit}/{self.experiment}/od_reading/interval")
if ads_interval_msg:
if ads_interval_msg and ads_interval_msg.payload:
ads_interval = float(ads_interval_msg.payload)
else:
return
Expand Down
2 changes: 1 addition & 1 deletion pioreactor/background_jobs/dosing_automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ def set_duration(self, duration: Optional[float]) -> None:
# there is a race condition here: self.run() will run immediately (see run_immediately), but the state of the job is not READY, since
# set_duration is run in the __init__ (hence the job is INIT). So we wait 2 seconds for the __init__ to finish, and then run.
# Later: in fact, we actually want this to run after an OD reading cycle so we have internal data, so it should wait a cycle of that.
run_after = 1.0 / config.getfloat("od_config", "samples_per_second")
run_after = 1.0 / config.getfloat("od_reading.config", "samples_per_second")

self.run_thread = RepeatedTimer(
self.duration * 60,
Expand Down
2 changes: 1 addition & 1 deletion pioreactor/background_jobs/growth_rate_calculating.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def __init__(
self.source_obs_from_mqtt = source_obs_from_mqtt
self.ignore_cache = ignore_cache
self.time_of_previous_observation: datetime | None = None
self.expected_dt = 1 / (60 * 60 * config.getfloat("od_config", "samples_per_second"))
self.expected_dt = 1 / (60 * 60 * config.getfloat("od_reading.config", "samples_per_second"))

def on_ready(self) -> None:
# this is here since the below is long running, and if kept in the init(), there is a large window where
Expand Down
2 changes: 1 addition & 1 deletion pioreactor/background_jobs/led_automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def set_duration(self, duration: float) -> None:
# set_duration is run in the __init__ (hence the job is INIT). So we wait 2 seconds for the __init__ to finish, and then run.
# Later: in fact, we actually want this to run after an OD reading cycle so we have internal data, so it should wait a cycle of that.
run_after = min(
1.0 / config.getfloat("od_config", "samples_per_second"), 10
1.0 / config.getfloat("od_reading.config", "samples_per_second"), 10
) # max so users aren't waiting forever to see lights come on...

self.run_thread = RepeatedTimer(
Expand Down
23 changes: 13 additions & 10 deletions pioreactor/background_jobs/od_reading.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ def __init__(
self.penalizer = penalizer
self.oversampling_count = oversampling_count

if "local_ac_hz" in config["od_config"]:
self.most_appropriate_AC_hz: Optional[float] = config.getfloat("od_config", "local_ac_hz")
if "local_ac_hz" in config["od_reading.config"]:
self.most_appropriate_AC_hz: Optional[float] = config.getfloat("od_reading.config", "local_ac_hz")
else:
self.most_appropriate_AC_hz = None

Expand Down Expand Up @@ -604,7 +604,9 @@ class PhotodiodeIrLedReferenceTrackerStaticInit(IrLedReferenceTracker):

def __init__(self, channel: pt.PdChannel) -> None:
super().__init__()
self.led_output_ema = ExponentialMovingAverage(config.getfloat("od_config", "pd_reference_ema"))
self.led_output_ema = ExponentialMovingAverage(
config.getfloat("od_reading.config", "pd_reference_ema")
)
self.led_output_emstd = ExponentialMovingStd(alpha=0.95, ema_alpha=0.8)
self.channel = channel
# self.logger.debug(f"Using PD channel {channel} as IR LED reference.")
Expand Down Expand Up @@ -684,10 +686,11 @@ def hydate_models_from_disk(self, channel_angle_map: dict[pt.PdChannel, pt.PdAng
calibration_data = decode(c[angle], type=structs.AnyODCalibration) # type: ignore
name = calibration_data.name

if config.get("od_config", "ir_led_intensity") != "auto" and (
calibration_data.ir_led_intensity != config.getfloat("od_config", "ir_led_intensity")
if config.get("od_reading.config", "ir_led_intensity") != "auto" and (
calibration_data.ir_led_intensity
!= config.getfloat("od_reading.config", "ir_led_intensity")
):
msg = f"The calibration `{name}` was calibrated with a different IR LED intensity ({calibration_data.ir_led_intensity} vs current: {config.getfloat('od_config', 'ir_led_intensity')}). Either re-calibrate, turn off calibration, or change the ir_led_intensity in the config.ini."
msg = f"The calibration `{name}` was calibrated with a different IR LED intensity ({calibration_data.ir_led_intensity} vs current: {config.getfloat('od_reading.config', 'ir_led_intensity')}). Either re-calibrate, turn off calibration, or change the ir_led_intensity in the config.ini."
self.logger.error(msg)
raise exc.CalibrationError(msg)
# confirm that PD channel is the same as when calibration was performed
Expand Down Expand Up @@ -870,7 +873,7 @@ def __init__(
self._set_for_iterating = threading.Event()

self.ir_channel: pt.LedChannel = self._get_ir_led_channel_from_configuration()
config_ir_led_intensity = config.get("od_config", "ir_led_intensity")
config_ir_led_intensity = config.get("od_reading.config", "ir_led_intensity")

self.ir_led_intensity: pt.LedIntensityValue
if config_ir_led_intensity == "auto":
Expand Down Expand Up @@ -1212,11 +1215,11 @@ def create_channel_angle_map(
def start_od_reading(
od_angle_channel1: Optional[pt.PdAngleOrREF] = None,
od_angle_channel2: Optional[pt.PdAngleOrREF] = None,
interval: Optional[float] = 1 / config.getfloat("od_config", "samples_per_second"),
interval: Optional[float] = 1 / config.getfloat("od_reading.config", "samples_per_second"),
fake_data: bool = False,
unit: Optional[str] = None,
experiment: Optional[str] = None,
use_calibration: bool = config.getboolean("od_config", "use_calibration"),
use_calibration: bool = config.getboolean("od_reading.config", "use_calibration"),
) -> ODReader:
"""
This function prepares ODReader and other necessary transformation objects. It's a higher level API than using ODReader.
Expand Down Expand Up @@ -1262,7 +1265,7 @@ def start_od_reading(
calibration_transformer = NullCalibrationTransformer() # type: ignore

if interval is not None:
penalizer = config.getfloat("od_config", "smoothing_penalizer", fallback=700.0) / interval
penalizer = config.getfloat("od_reading.config", "smoothing_penalizer", fallback=700.0) / interval
else:
penalizer = 0.0

Expand Down
21 changes: 11 additions & 10 deletions pioreactor/background_jobs/stirring.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,7 @@ class Stirrer(BackgroundJob):
"duty_cycle": {"datatype": "float", "settable": True, "unit": "%"},
}

duty_cycle: float = config.getfloat(
"stirring", "initial_duty_cycle"
) # only used if calibration isn't defined.
duty_cycle: float = 0
_previous_duty_cycle: float = 0
_measured_rpm: Optional[float] = None

Expand All @@ -203,7 +201,6 @@ def __init__(
unit: str,
experiment: str,
rpm_calculator: Optional[RpmCalculator] = None,
hertz: float = config.getfloat("stirring", "pwm_hz"),
) -> None:
super(Stirrer, self).__init__(unit=unit, experiment=experiment)
self.rpm_calculator = rpm_calculator
Expand Down Expand Up @@ -231,7 +228,9 @@ def __init__(
return

pin: pt.GpioPin = hardware.PWM_TO_PIN[channel]
self.pwm = PWM(pin, hertz, unit=self.unit, experiment=self.experiment)
self.pwm = PWM(
pin, config.getfloat("stirring.config", "pwm_hz"), unit=self.unit, experiment=self.experiment
)
self.pwm.lock()

if target_rpm is not None and self.rpm_calculator is not None:
Expand All @@ -257,7 +256,7 @@ def __init__(

# set up thread to periodically check the rpm
self.rpm_check_repeated_thread = RepeatedTimer(
config.getfloat("stirring", "duration_between_updates_seconds", fallback=23.0),
config.getfloat("stirring.config", "duration_between_updates_seconds", fallback=23.0),
self.poll_and_update_dc,
job_name=self.job_name,
run_immediately=True,
Expand Down Expand Up @@ -478,10 +477,10 @@ def block_until_rpm_is_close_to_target(


def start_stirring(
target_rpm: float = config.getfloat("stirring", "target_rpm", fallback=400),
target_rpm: float = config.getfloat("stirring.config", "target_rpm", fallback=400),
unit: Optional[str] = None,
experiment: Optional[str] = None,
use_rpm: bool = config.getboolean("stirring", "use_rpm", fallback="true"),
use_rpm: bool = config.getboolean("stirring.config", "use_rpm", fallback="true"),
) -> Stirrer:
unit = unit or get_unit_name()
experiment = experiment or get_assigned_experiment_name(unit)
Expand All @@ -508,12 +507,14 @@ def start_stirring(
@click.command(name="stirring")
@click.option(
"--target-rpm",
default=config.getfloat("stirring", "target_rpm", fallback=400),
default=config.getfloat("stirring.config", "target_rpm", fallback=400),
help="set the target RPM",
show_default=True,
type=click.FloatRange(0, 1500, clamp=True),
)
@click.option("--use-rpm/--ignore-rpm", default=config.getboolean("stirring", "use_rpm", fallback="true"))
@click.option(
"--use-rpm/--ignore-rpm", default=config.getboolean("stirring.config", "use_rpm", fallback="true")
)
def click_stirring(target_rpm: float, use_rpm: bool) -> None:
"""
Start the stirring of the Pioreactor.
Expand Down
2 changes: 1 addition & 1 deletion pioreactor/cli/pios.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def sync_config_files(unit: str, shared: bool, specific: bool) -> None:

except Exception as e:
click.echo(
f"Did you forget to create a config_{unit}.ini to deploy to {unit}?",
f"Error syncing config_{unit}.ini to {unit} - do they exist?",
err=True,
)
raise e
Expand Down
12 changes: 6 additions & 6 deletions pioreactor/tests/test_growth_rate_calculating.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ def test_end_to_end(self) -> None:
experiment = "test_end_to_end"

interval = 0.1
config["od_config"]["samples_per_second"] = "0.2"
config["od_reading.config"]["samples_per_second"] = "0.2"

with start_od_reading(
"REF",
Expand All @@ -455,7 +455,7 @@ def test_180_angle(self) -> None:
unit = get_unit_name()
experiment = "test_180_angle"
samples_per_second = 0.2
config["od_config"]["samples_per_second"] = str(samples_per_second)
config["od_reading.config"]["samples_per_second"] = str(samples_per_second)
config["od_config.photodiode_channel"]["1"] = "180"
config["od_config.photodiode_channel"]["2"] = None # type: ignore

Expand Down Expand Up @@ -506,7 +506,7 @@ def test_90_angle(self) -> None:
unit = get_unit_name()
experiment = "test_90_angle"
samples_per_second = 0.2
config["od_config"]["samples_per_second"] = str(samples_per_second)
config["od_reading.config"]["samples_per_second"] = str(samples_per_second)
config["od_config.photodiode_channel"]["1"] = "90"
config["od_config.photodiode_channel"]["2"] = None # type: ignore

Expand Down Expand Up @@ -787,7 +787,7 @@ def test_single_outlier_spike_gets_absorbed(self) -> None:
unit = get_unit_name()
experiment = "test_single_outlier_spike_gets_absorbed"

config["od_config"]["samples_per_second"] = "0.2"
config["od_reading.config"]["samples_per_second"] = "0.2"

# clear mqtt
publish(
Expand Down Expand Up @@ -885,7 +885,7 @@ def test_baseline_shift_gets_absorbed(self) -> None:
unit = get_unit_name()
experiment = "test_baseline_shift_gets_absorbed"

config["od_config"]["samples_per_second"] = "0.2"
config["od_reading.config"]["samples_per_second"] = "0.2"

# clear mqtt
publish(
Expand Down Expand Up @@ -950,7 +950,7 @@ def test_massive_outlier_spike_gets_absorbed(self) -> None:
unit = get_unit_name()
experiment = "test_massive_outlier_spike_gets_absorbed"

config["od_config"]["samples_per_second"] = "0.2"
config["od_reading.config"]["samples_per_second"] = "0.2"

# clear mqtt
publish(
Expand Down
2 changes: 1 addition & 1 deletion pioreactor/tests/test_mqtt_to_db_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def test_dosing_events_land_in_db() -> None:

def test_kalman_filter_entries() -> None:
config["storage"]["database"] = "test.sqlite"
config["od_config"]["samples_per_second"] = "0.2"
config["od_reading.config"]["samples_per_second"] = "0.2"
config["od_config.photodiode_channel"]["1"] = "135"
config["od_config.photodiode_channel"]["2"] = "90"

Expand Down
Loading

0 comments on commit 3a8e3ae

Please sign in to comment.