Skip to content

Commit da42432

Browse files
Merge branch 'release/25.4.9'
* release/25.4.9: bump version recopy the previous update scripts self-tests shouldn't use calibrations new pump calibration protocol that allows users to provide ml to calibrate to some bug fixes + improvements
2 parents 32c57ba + 86550a5 commit da42432

File tree

15 files changed

+374
-50
lines changed

15 files changed

+374
-50
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
### 25.4.9
2+
3+
#### Bug fixes
4+
- Fix OD Reading not displaying correctly on Pioreactor pages
5+
- Fix duplicates in Raw OD Reading chart's legend
6+
- Improvements to pump calibration flow that will ask user what volumes they wish to calibrate for.
7+
- Fix self-test "REF is correct magnitude"
8+
- self-tests don't use calibrated OD readings anymore.
9+
110
### 25.4.3
211

312
#### Enhancements

pioreactor/actions/self_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def test_REF_is_in_correct_position(managed_state, logger: CustomLogger, unit: s
8585
unit=unit,
8686
fake_data=is_testing_env(),
8787
experiment=experiment,
88+
calibration=False,
8889
) as od_stream:
8990
st.block_until_rpm_is_close_to_target(abs_tolerance=150, timeout=10)
9091

@@ -318,7 +319,7 @@ def test_REF_is_lower_than_0_dot_256_volts(
318319
samples = []
319320

320321
for i in range(6):
321-
samples.append(adc_reader.take_reading()[reference_channel])
322+
samples.append(adc_reader.take_reading()[reference_channel].reading)
322323

323324
assert (
324325
0.02 < mean(samples) < 0.500
@@ -352,6 +353,7 @@ def test_PD_is_near_0_volts_for_blank(
352353
unit=unit,
353354
fake_data=is_testing_env(),
354355
experiment=experiment,
356+
calibration=False,
355357
) as od_stream:
356358
for i, reading in enumerate(od_stream, start=1):
357359
signals.append(reading.ods[signal_channel].od)

pioreactor/background_jobs/base.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -691,11 +691,7 @@ def exit_gracefully(reason: int | str, *args) -> None:
691691
# keyboard interrupt
692692
append_signal_handlers(
693693
signal.SIGINT,
694-
[
695-
exit_gracefully,
696-
# add a "ignore all future SIGINTs" onto the top of the stack.
697-
lambda *args: signal.signal(signal.SIGINT, signal.SIG_IGN),
698-
],
694+
[exit_gracefully],
699695
)
700696

701697
try:

pioreactor/background_jobs/monitor.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ def MAX_TEMP_TO_SHUTDOWN(cls) -> float:
8888
)
8989
return 66.0 if is_20ml_v1 else 85.0
9090

91+
@classproperty
92+
def MAX_TEMP_TO_SHUTDOWN_IF_NO_TEMP_AUTOMATION(cls) -> float:
93+
return 65.0
94+
9195
job_name = "monitor"
9296
published_settings = {
9397
"computer_statistics": {"datatype": "json", "settable": False},
@@ -357,6 +361,19 @@ def check_heater_pcb_temperature(self) -> None:
357361
)
358362

359363
subprocess.call("sudo shutdown now --poweroff", shell=True)
364+
365+
elif observed_tmp >= self.MAX_TEMP_TO_SHUTDOWN_IF_NO_TEMP_AUTOMATION and not utils.is_pio_job_running(
366+
"temperature_automation"
367+
):
368+
# errant PWM?
369+
# false positive: small chance this is in an incubator?
370+
371+
self.logger.error(
372+
f"Detected an extremely high temperature but heating is turned off, {observed_tmp} ℃ on the heating PCB - shutting down for safety."
373+
)
374+
375+
subprocess.call("sudo shutdown now --poweroff", shell=True)
376+
360377
self.logger.debug(f"Heating PCB temperature at {round(observed_tmp)} ℃.")
361378

362379
def check_for_mqtt_connection_to_leader(self) -> None:

pioreactor/background_jobs/od_reading.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -665,9 +665,11 @@ def hydate_models(self, calibration_data: structs.ODCalibration | None) -> None:
665665
)
666666

667667
def _hydrate_model(self, calibration_data: structs.ODCalibration) -> Callable[[pt.Voltage], pt.OD]:
668-
if (calibration_data.x != "OD600") or (calibration_data.y != "Voltage"):
669-
self.logger.error(f"Calibration {calibration_data.calibration_name} is not for OD600.")
670-
raise exc.CalibrationError(f"Calibration {calibration_data.calibration_name} is not for OD600.")
668+
if (
669+
calibration_data.y != "Voltage"
670+
): # don't check for OD600 - we can allow other non-OD600 calibrations
671+
self.logger.error(f"Calibration {calibration_data.calibration_name} has wrong type.")
672+
raise exc.CalibrationError(f"Calibration {calibration_data.calibration_name} has wrong type.")
671673

672674
def _calibrate_signal(observed_voltage: pt.Voltage) -> pt.OD:
673675
min_OD, max_OD = min(calibration_data.recorded_data["x"]), max(

pioreactor/calibrations/pump_calibration.py

Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,26 @@ def introduction(pump_device) -> None:
5252

5353
logging.disable(logging.WARNING)
5454

55+
try:
56+
channel_pump_is_configured_for = config.get("PWM_reverse", pump_device.removesuffix("_pump"))
57+
except KeyError:
58+
echo(
59+
red(
60+
f"❌ {pump_device} is not present in config.ini. Please add it to the [PWM] section and try again."
61+
)
62+
)
63+
raise Abort()
64+
5565
echo(
5666
f"""This routine will calibrate the {pump_device} on your current Pioreactor. You'll need:
5767
5868
1. A Pioreactor
59-
2. A vial placed on a scale with accuracy at least 0.1g
69+
2. A vial is placed on a scale that measures weight with a minimum resolution of 0.1 grams.
6070
OR an accurate graduated cylinder.
6171
3. A larger container filled with water
62-
4. {pump_device} connected to the correct PWM channel (1, 2, 3, or 4) as determined in your configuration.
72+
4. {pump_device} connected to the correct PWM channel {channel_pump_is_configured_for}.
6373
64-
We will dose for a set duration, you'll measure how much volume was expelled, and then record it back here. After doing this a few times, we can construct a calibration line for this pump.
74+
We will dose for a set duration, you'll measure how much volume was expelled, and then record it back here. After doing this a few times, we can construct a calibration curve for this pump.
6575
"""
6676
)
6777
confirm(green("Proceed?"), abort=True, default=True)
@@ -100,15 +110,8 @@ def setup(
100110
pump_device: PumpCalibrationDevices, execute_pump: Callable, hz: float, dc: float, unit: str
101111
) -> None:
102112
# set up...
103-
try:
104-
channel_pump_is_configured_for = config.get("PWM_reverse", pump_device.removesuffix("_pump"))
105-
except KeyError:
106-
echo(
107-
red(
108-
f"❌ {pump_device} is not present in config.ini. Please add it to the [PWM] section and try again."
109-
)
110-
)
111-
raise Abort()
113+
channel_pump_is_configured_for = config.get("PWM_reverse", pump_device.removesuffix("_pump"))
114+
112115
clear()
113116
echo()
114117
echo(green(bold("Step 2")))
@@ -128,7 +131,7 @@ def setup(
128131

129132
while not confirm(green("Ready to start pumping?")):
130133
pass
131-
134+
echo()
132135
echo(
133136
bold(
134137
"Press CTRL+C when the tubes are completely filled with water and there are no air pockets in the tubes."
@@ -163,6 +166,7 @@ def setup(
163166

164167

165168
def choose_settings() -> tuple[float, float]:
169+
clear()
166170
hz = prompt(
167171
style(green("Optional: Enter frequency of PWM. [enter] for default 250 hz")),
168172
type=click.FloatRange(0.1, 10000),
@@ -207,14 +211,8 @@ def plot_data(x, y, title, x_min=None, x_max=None, interpolation_curve=None, hig
207211

208212

209213
def run_tests(
210-
execute_pump: Callable,
211-
hz: float,
212-
dc: float,
213-
min_duration: float,
214-
max_duration: float,
215-
pump_device: PumpCalibrationDevices,
216-
unit: str,
217-
) -> tuple[list[float], list[float]]:
214+
execute_pump: Callable, hz: float, dc: float, unit: str, mls_to_calibrate_for: list[float]
215+
) -> tuple[list[float], list[float], float, float]:
218216
clear()
219217
echo()
220218
echo(green(bold("Step 3")))
@@ -232,6 +230,46 @@ def run_tests(
232230
recorded_data={"x": [], "y": []},
233231
)
234232

233+
tracer_duration = 1.0
234+
235+
echo("We will run the pump for a set amount of time, and you will measure how much liquid is expelled.")
236+
echo("Use a small container placed on top of an accurate weighing scale.")
237+
echo("Hold the end of the outflow tube above so the container catches the expelled liquid.")
238+
echo()
239+
240+
while not confirm(style(green(f"Ready to test {tracer_duration:.2f}s?"))):
241+
pass
242+
243+
execute_pump(
244+
duration=tracer_duration,
245+
source_of_event="pump_calibration",
246+
unit=get_unit_name(),
247+
experiment=get_testing_experiment_name(),
248+
calibration=empty_calibration,
249+
)
250+
251+
while True:
252+
r = prompt(
253+
style(green("Enter amount of water expelled (g or ml), or REDO")),
254+
confirmation_prompt=style(green("Repeat for confirmation")),
255+
)
256+
if r == "REDO":
257+
clear()
258+
echo()
259+
continue
260+
261+
try:
262+
tracer_ml = float(r)
263+
clear()
264+
echo()
265+
break
266+
except ValueError:
267+
echo(red("Not a number - retrying."))
268+
269+
# calculate min and max duration based on tracer_ml
270+
min_duration = min(mls_to_calibrate_for) * 0.9 / tracer_ml * tracer_duration
271+
max_duration = max(mls_to_calibrate_for) * 1.1 / tracer_ml * tracer_duration
272+
235273
results: list[float] = []
236274
durations_to_test = [min_duration] * 4 + [(min_duration + max_duration) / 2] * 2 + [max_duration] * 4
237275
n_samples = len(durations_to_test)
@@ -264,7 +302,7 @@ def run_tests(
264302
)
265303
)
266304
)
267-
while not confirm(style(green(f"Ready to test {duration:.2f}s?"))):
305+
while not confirm(style(green(f"Ready to test {duration:.1f}s?"))):
268306
pass
269307

270308
execute_pump(
@@ -292,7 +330,7 @@ def run_tests(
292330
except ValueError:
293331
echo(red("Not a number - retrying."))
294332

295-
return durations_to_test, results
333+
return durations_to_test, results, min_duration, max_duration
296334

297335

298336
def save_results(
@@ -322,8 +360,28 @@ def save_results(
322360
return pump_calibration_result
323361

324362

363+
def get_user_calibrations() -> list[float]:
364+
clear()
365+
mls = []
366+
r = prompt(
367+
green("Enter the volume you wish to calibrate around (mL). [enter] to use default 1.0 ml"),
368+
default=1.0,
369+
type=float,
370+
)
371+
mls.append(float(r))
372+
while click.confirm(green("Do you want to add another value?")):
373+
r = prompt(
374+
green("Enter another volume you wish to calibrate around (mL)"),
375+
default=1.0,
376+
type=float,
377+
)
378+
mls.append(float(r))
379+
380+
return mls
381+
382+
325383
def run_pump_calibration(
326-
pump_device, min_duration: float = 0.40, max_duration: float = 1.5
384+
pump_device,
327385
) -> structs.SimplePeristalticPumpCalibration:
328386
unit = get_unit_name()
329387
experiment = get_assigned_experiment_name(unit)
@@ -345,19 +403,22 @@ def run_pump_calibration(
345403
raise ValueError()
346404

347405
name = get_metadata_from_user(pump_device)
406+
mls_to_calibrate_for = get_user_calibrations()
348407

349-
is_ready = True
350-
while is_ready:
408+
settings_are_correct = False
409+
while not settings_are_correct:
351410
hz, dc = choose_settings()
352411
setup(pump_device, execute_pump, hz, dc, unit)
353412

354-
is_ready = confirm(
413+
settings_are_correct = not confirm(
355414
style(green("Do you want to change the frequency or duty cycle?")),
356415
prompt_suffix=" ",
357416
default=False,
358417
)
359418

360-
durations, volumes = run_tests(execute_pump, hz, dc, min_duration, max_duration, pump_device, unit)
419+
durations, volumes, min_duration, max_duration = run_tests(
420+
execute_pump, hz, dc, unit, mls_to_calibrate_for
421+
)
361422

362423
(slope, std_slope), (
363424
bias,

pioreactor/tests/test_calibrations.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ def test_save_and_load_calibration(temp_calibration_dir) -> None:
3838
created_at=datetime.now(timezone.utc),
3939
curve_data_=[1.0, 2.0, 3.0],
4040
curve_type="poly",
41-
x="voltage",
42-
y="od600",
4341
recorded_data={"x": [0.1, 0.2], "y": [0.3, 0.4]},
4442
ir_led_intensity=1.23,
4543
angle="90",

pioreactor/utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def __init__(
174174
# this only works on the main thread.
175175
append_signal_handlers(signal.SIGTERM, [self._exit])
176176
append_signal_handlers(
177-
signal.SIGINT, [self._exit, lambda *args: signal.signal(signal.SIGINT, signal.SIG_IGN)]
177+
signal.SIGINT, [self._exit]
178178
) # ignore future sigints so we clean up properly.
179179
except ValueError:
180180
pass

0 commit comments

Comments
 (0)