Skip to content

Commit

Permalink
push back the adc change to later
Browse files Browse the repository at this point in the history
  • Loading branch information
CamDavidsonPilon committed Feb 6, 2025
1 parent 33c2f17 commit d4b1158
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 110 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

#### Enhancements
- new OD calibration using standards (requires multiple vials). Try `pio calibrations run --device od`. This was inspired by the plugin by @odcambc.
- new RP2040 firmware improvements (v0.4) => faster response over i2c.
- Improved chart colors in the UI
- The OD reading CLI has a new option, `--snapshot`, that will start the job, take a single reading, and exit. This is useful for scripting purposes.
- A new CLI for pumps: `pio run pumps`. Add pumps as options:
Expand All @@ -18,7 +17,7 @@
- Run multiple experiment profiles per experiment!
- Specify which Pioreactor to update on the Updates page (option is only available with release archives.)
- Choose the level of detail on the new Event Logs page.
- Added index to stirring_rates table.
- Previously, when a worker's web server is down, it would halt an update from proceeding (since it can't send the command). Now, leader will try the webserver, and if it observes a 5xx error, will attempt an SSH communication.

#### Web API changes

Expand Down
85 changes: 19 additions & 66 deletions pioreactor/calibrations/od_calibration_using_standards.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"""
from __future__ import annotations

from math import log2
from time import sleep
from typing import cast

Expand Down Expand Up @@ -56,12 +55,10 @@ def introduction() -> None:

clear()
echo(
"""This routine will calibrate the current Pioreactor to (offline) OD600 readings. You'll need:
1. The Pioreactor you wish to calibrate (the one you are using)
2. At least 10mL of a culture with the highest density you'll ever observe, and its OD600 measurement
3. A micro-pipette, or accurate tool to dispense 1ml of liquid.
4. Accurate 10mL measurement tool (ex: graduated cylinder)
5. Sterile media, amount to be determined shortly.
"""This routine will calibrate the current Pioreactor to (offline) OD600 readings using a set of standards. You'll need:
1. A Pioreactor
2. A set of OD600 standards in Pioreactor vials (at least 10 mL in each vial), each with a stirbar
3. One of the standards should be a blank (no cells, only media).
"""
)

Expand Down Expand Up @@ -91,7 +88,7 @@ def get_name_from_user() -> str:
return name


def get_metadata_from_user() -> tuple[pt.OD600, pt.OD600, pt.mL, pt.PdAngle, pt.PdChannel]:
def get_metadata_from_user() -> tuple[pt.PdAngle, pt.PdChannel]:
if config.get("od_reading.config", "ir_led_intensity") == "auto":
echo(
red(
Expand All @@ -100,49 +97,6 @@ def get_metadata_from_user() -> tuple[pt.OD600, pt.OD600, pt.mL, pt.PdAngle, pt.
)
raise click.Abort()

initial_od600 = prompt(
green("Provide the OD600 measurement of your initial, high density, culture"),
type=click.FloatRange(min=0.01, clamp=False),
)

minimum_od600 = prompt(
green("Provide the minimum OD600 measurement you wish to calibrate to"),
type=click.FloatRange(min=0, max=initial_od600, clamp=False),
)

while minimum_od600 >= initial_od600:
minimum_od600 = cast(
pt.OD600,
prompt(
"The minimum OD600 measurement must be less than the initial OD600 culture measurement",
type=click.FloatRange(min=0, max=initial_od600, clamp=False),
),
)

if minimum_od600 == 0:
minimum_od600 = 0.01

dilution_amount = prompt(
green("Provide the volume to be added to your vial each iteration (default = 1 mL)"),
default=1,
type=click.FloatRange(min=0.01, max=10, clamp=False),
)

number_of_points = int(log2(initial_od600 / minimum_od600) * (10 / dilution_amount))

echo(f"This will require {number_of_points} data points.")
echo(f"You will need at least {number_of_points * dilution_amount + 10}mL of media available.")
confirm(green("Continue?"), abort=True, default=True)

if "REF" not in config["od_config.photodiode_channel_reverse"]:
echo(
red(
"REF required for OD calibration. Set an input to REF in [od_config.photodiode_channel] in your config."
)
)
raise click.Abort()
# technically it's not required? we just need a specific PD channel to calibrate from.

ref_channel = config["od_config.photodiode_channel_reverse"]["REF"]
pd_channel = cast(pt.PdChannel, "1" if ref_channel == "2" else "2")

Expand All @@ -154,7 +108,7 @@ def get_metadata_from_user() -> tuple[pt.OD600, pt.OD600, pt.mL, pt.PdAngle, pt.
default=True,
)
angle = cast(pt.PdAngle, config["od_config.photodiode_channel"][pd_channel])
return initial_od600, minimum_od600, dilution_amount, angle, pd_channel
return angle, pd_channel


def setup_HDC_instructions() -> None:
Expand Down Expand Up @@ -243,13 +197,14 @@ def get_voltage_from_adc() -> float:
od_readings2 = od_reader.record_from_adc()
return 0.5 * (od_readings1.ods[signal_channel].od + od_readings2.ods[signal_channel].od)

for _ in range(4):
for _ in range(3):
# warm up
od_reader.record_from_adc()

while True:
click.echo("Recording next standard.")
standard_od = click.prompt("Enter OD600 measurement", type=float)
click.clear()
click.echo("Recording new standard.")
standard_od = click.prompt(green("Enter OD600 measurement of current vial"), type=float)
for i in range(4):
click.echo(".", nl=False)
sleep(0.5)
Expand All @@ -276,16 +231,17 @@ def get_voltage_from_adc() -> float:
)
click.echo()

if not click.confirm("Record another OD600 standard?", default=True):
if not click.confirm(green("Record another OD600 standard?"), default=True):
break

click.echo()
click.echo(click.style("Stop❗", fg="red"))
click.echo("Carefully remove vial and replace with next standard.")
click.echo("Confirm vial outside is dry and clean.")
while not click.confirm("Continue?", default=True):
click.echo("Remove old vial.")
click.echo("Replace with new vial: confirm vial is dry and clean.")
click.echo()
while not click.confirm(green("Confirm vial is placed in Pioreactor?"), default=True):
pass
st.set_state("ready")
click.echo("Starting stirring.")
st.block_until_rpm_is_close_to_target(abs_tolerance=120)
sleep(1.0)

Expand All @@ -300,9 +256,9 @@ def get_voltage_from_adc() -> float:
y_label="Voltage",
)
click.echo("Add media blank standard.")
od600_blank = click.prompt("What is the OD600 of your blank?", type=float)
click.echo("Confirm vial outside is dry and clean. Place into Pioreactor.")
while not click.confirm("Continue?", default=True):
od600_blank = click.prompt(green("What is the OD600 of your blank?"), type=float)
click.echo("Confirm blank vial outside is dry and clean. Place into Pioreactor.")
while not click.confirm(green("Continue?"), default=True):
pass

voltages.append(get_voltage_from_adc())
Expand All @@ -326,9 +282,6 @@ def run_od_calibration() -> structs.ODCalibration:
raise click.Abort()

(
initial_od600,
minimum_od600,
dilution_amount,
angle,
pd_channel,
) = get_metadata_from_user()
Expand Down
14 changes: 11 additions & 3 deletions pioreactor/cli/calibrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
from pioreactor.calibrations.utils import plot_data


def green(string: str) -> str:
return click.style(string, fg="green")


@click.group(short_help="calibration utils")
def calibration() -> None:
"""
Expand Down Expand Up @@ -86,12 +90,14 @@ def run_calibration(ctx, device: str, protocol_name: str | None, y: bool) -> Non
click.clear()
click.echo()
click.echo(f"Available protocols for {device}:")
click.echo()
for protocol in calibration_protocols.get(device, {}).values():
click.echo(click.style(f" • {protocol.protocol_name}", bold=True))
click.echo(f" Description: {protocol.description}")
click.echo()
protocol_name = click.prompt(
"Choose a protocol", type=click.Choice(list(calibration_protocols.get(device, {}).keys()))
green("Choose a protocol"),
type=click.Choice(list(calibration_protocols.get(device, {}).keys())),
)

assistant = calibration_protocols.get(device, {}).get(protocol_name)
Expand All @@ -111,7 +117,8 @@ def run_calibration(ctx, device: str, protocol_name: str | None, y: bool) -> Non

if not y:
if click.confirm(
f"Do you want to set this calibration as the active calibration for {device}?", default=True
green(f"Do you want to set this calibration as the active calibration for {device}?"),
default=True,
):
calibration_struct.set_as_active_calibration_for_device(device)
click.echo(f"Set {calibration_struct.calibration_name} as the active calibration for {device}.")
Expand All @@ -122,6 +129,7 @@ def run_calibration(ctx, device: str, protocol_name: str | None, y: bool) -> Non
else:
calibration_struct.set_as_active_calibration_for_device(device)

click.echo()
click.echo(
f"Calibration '{calibration_struct.calibration_name}' of device '{device}' saved to {out_file} ✅"
)
Expand Down Expand Up @@ -186,7 +194,7 @@ def set_active_calibration(device: str, calibration_name: str | None) -> None:
@calibration.command(name="delete")
@click.option("--device", required=True, help="Which calibration device to delete from.")
@click.option("--name", "calibration_name", required=True, help="Which calibration name to delete.")
@click.confirmation_option(prompt="Are you sure you want to delete this calibration?")
@click.confirmation_option(prompt=green("Are you sure you want to delete this calibration?"))
def delete_calibration(device: str, calibration_name: str) -> None:
"""
Delete a calibration file from local storage.
Expand Down
39 changes: 14 additions & 25 deletions pioreactor/cli/pios.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,14 @@ def update(

logger = create_logger("update", unit=get_unit_name(), experiment=UNIVERSAL_EXPERIMENT)
options: dict[str, str | None] = {}
args = ""

if source is not None:
options["source"] = source
elif branch is not None:
if branch is not None:
options["branch"] = branch
args = f"--branch {branch}"
elif source is not None:
options["source"] = source
args = f"--source {source}"

def _thread_function(unit: str) -> tuple[bool, dict]:
try:
Expand All @@ -304,12 +307,6 @@ def _thread_function(unit: str) -> tuple[bool, dict]:
f"Unable to update on {unit} due to server error: {e}. Attempting SSH method..."
)
try:
args: str
if source is not None:
args = f"--source {source}"
elif branch is not None:
args = f"--branch {branch}"

ssh(resolve_to_address(unit), f"pio update {args}")
return True, {"unit": unit}
except SSHError as e:
Expand Down Expand Up @@ -362,14 +359,18 @@ def update_app(

logger = create_logger("update", unit=get_unit_name(), experiment=UNIVERSAL_EXPERIMENT)
options: dict[str, str | None] = {}
args = ""

# only one of these three is possible, mutually exclusive
if version is not None:
options["version"] = version
args = f"--version {version}"
elif branch is not None:
options["branch"] = branch
args = f"--branch {branch}"
elif source is not None:
options["source"] = source
args = f"--source {source}"

if repo is not None:
options["repo"] = repo
Expand All @@ -384,14 +385,6 @@ def _thread_function(unit: str) -> tuple[bool, dict]:
except HTTPException as e:
logger.error(f"Unable to update on {unit} due to server error: {e}. Attempting SSH method...")
try:
args: str
if source is not None:
args = f"--source {source}"
elif branch is not None:
args = f"--branch {branch}"
elif version is not None:
args = f"--version {version}"

ssh(resolve_to_address(unit), f"pio update app {args}")
return True, {"unit": unit}
except SSHError as e:
Expand Down Expand Up @@ -443,14 +436,18 @@ def update_ui(

logger = create_logger("update", unit=get_unit_name(), experiment=UNIVERSAL_EXPERIMENT)
options: dict[str, str | None] = {}
args = ""

# only one of these three is possible, mutually exclusive
if version is not None:
options["version"] = version
args = f"--version {version}"
elif branch is not None:
options["branch"] = branch
args = f"--branch {branch}"
elif source is not None:
options["source"] = source
args = f"--source {source}"

if repo is not None:
options["repo"] = repo
Expand All @@ -465,14 +462,6 @@ def _thread_function(unit: str) -> tuple[bool, dict]:
except HTTPException as e:
logger.error(f"Unable to update on {unit} due to server error: {e}. Attempting SSH method...")
try:
args: str
if source is not None:
args = f"--source {source}"
elif branch is not None:
args = f"--branch {branch}"
elif version is not None:
args = f"--version {version}"

ssh(resolve_to_address(unit), f"pio update ui {args}")
return True, {"unit": unit}
except SSHError as e:
Expand Down
9 changes: 5 additions & 4 deletions pioreactor/utils/adcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ class Pico_ADC(_ADC):
def __init__(self) -> None:
# set up i2c connection to hardware.ADC
self.i2c = I2C(hardware.SCL, hardware.SDA)
assert self.get_firmware_version() == (0, 4), "Firmware version mismatch."
self.scale = 16
# assert self.get_firmware_version() == (0, 4), "Firmware version mismatch."

def read_from_channel(self, channel: pt.AdcChannel) -> pt.AnalogValue:
assert 0 <= channel <= 3
Expand All @@ -115,13 +116,13 @@ def get_firmware_version(self) -> tuple[int, int]:
return (result[1], result[0])

def from_voltage_to_raw(self, voltage: pt.Voltage) -> pt.AnalogValue:
return int((voltage / 3.3) * 4095 * 32)
return int((voltage / 3.3) * 4095 * self.scale)

def from_voltage_to_raw_precise(self, voltage: pt.Voltage) -> float:
return (voltage / 3.3) * 4095 * 32
return (voltage / 3.3) * 4095 * self.scale

def from_raw_to_voltage(self, raw: pt.AnalogValue) -> pt.Voltage:
return (raw / 4095 / 32) * 3.3
return (raw / 4095 / self.scale) * 3.3

def check_on_gain(self, value: pt.Voltage, tol: float = 0.85) -> None:
# pico has no gain.
Expand Down
10 changes: 0 additions & 10 deletions update_scripts/upcoming/update.sql

This file was deleted.

0 comments on commit d4b1158

Please sign in to comment.