Adding a new calibration type
- A calibration relates two variables together. One of the variables is what we can vary (in theory), and the other quantity is the target.
- The calibration relates the two via a calibration curve, which is just a mapping between the two variables.
- The Pioreactor has many devices that can be calibrated. For example, the OD system, media pump, waste pump, or stirring, can all be improved with a calibration being given. A device can have multiple calibrations, but only one can be active at a time. The device looks for the active calibration, and uses that in practice.
- A protocol can be used to create a calibration for a device. Devices could have multiple protocols that can create calibrations.
- A calibration can be given to possible multiple devices. Example: the same calibration can be used for waste and media pumps.
In practice, calibrations are stored as YAML files on the Pioreactor, in ~/.pioreactor/storage/calibrations , divided into directories by the associated device. By keeping the calibrations as files (instead of in a database) makes moving, sharing, and editing calibrations really easy.
CLI tools
There is a useful CLI available to manage calibrations, too: pio calibrations --help.
Creating a new device
Create a new device is easy: just add a new folder to ~/.pioreactor/storage/calibrations with the device name. This device name is used elsewhere, so keep it simple and easy to use.
For example, let's create a pH device, named ph. SSH into your Pioreactor and run
mkdir ~/.pioreactor/storage/calibrations/ph
chown pioreactor:www-data ~/.pioreactor/storage/calibrations/ph
The chown is required so that the webserver can access that folder to read from it.
Creating a new calibration type
A device can be calibrated by different types of calibrations. For example, you many want to relate the volume that pumps move to the duration the pump is ON for, or relate the volume to the amount of power applied in 1 second interval. These are different calibration types. To create a new calibration type, you need to define its unique schema.
Continuing our pH example, the calibration type for it might look like:
from pioreactor.structs import CalibrationBase # see this class for the full list of fields
class PHBufferCalibration(CalibrationBase, kw_only=True, tag="ph_buffer"):
x: str = "pH" # required
y: str = "Voltage" # required
# add some optional metadata fields
buffer_solution: t.Literal["4.01", "7.00", "10.01"]
electrode_type: str
# not required, but helpful
def voltage_to_ph(self, voltage: float) -> float:
return self.y_to_x(voltage)
def ph_to_voltage(self, ph: float) -> float:
return self.x_to_y(ph)
The tag should be unique for this calibration type. For example, if we instead had another pH calibration type that used optics instead of buggers, we could define another pH calibration type as follows:
class PHOpticsCalibration(CalibrationBase, kw_only=True, tag="optical_ph"):
x: str = "pH"
y: str = "Lumens"
It's optional, but we also defined some helper functions voltage_to_ph and ph_to_voltage to easily map between the variables. Internally, They call x_to_y and y_to_x functions which are always available on a calibration object. They do the hard math behind mapping variables to each other.
Tips
- use session-based
SessionStepflows to define CLI and UI behavior in one place. - the pair
(device, calibration_name)must be unique. The final directory structure looks like~/.pioreactor/storage/calibrations/<device>/<calibration_name>.yaml - The
xvariable should be the independent variable - the variable that can (in theory) be set by you, and the measurement variableyfollows. For example, in the default OD calibration, the independent variable is the OD, and the dependent variable is the Pioreactor's sensor's voltage. This is because we can vary the OD as we wish (add more culture...), and the Pioreactor's sensor will detect different values. - Another way to look at this is: "where does error exist"? Typically, there will be error in the "measurement" variable (voltage for OD calibration, RPM measurement for stirring calibration, etc.). In practice, we only have the measurement variable, and wish to go "back" to the original variable.
(Optional) Creating a new protocol for an existing device
If you want to add a custom script to create a calibration on the Pioreactor, you can do that by creating a new protocol.
Define a CalibrationProtocol subclass that will hold metadata for your protocol. It should have a run method that returns a calibration (a subclass of CalibrationBase - see above). If your protocol supports session-based UI/CLI flows, also define a step registry and a start_session classmethod.
from pioreactor.calibrations import CalibrationProtocol
from pioreactor.utils.timing import current_utc_datetime
from pioreactor import whoami
class BufferBasedPHProtocol(CalibrationProtocol):
target_device = "ph"
protocol_name = "buffer_based"
description = "Calibrate the pH sensor using buffer solutions"
step_registry = PH_STEPS
@classmethod
def start_session(cls, target_device: str) -> CalibrationSession:
return start_ph_buffer_session(target_device)
def run(self, target_device: str):
return run_ph_buffer_calibration()
def run_ph_buffer_calibration():
# run the calibration, look at other calibration examples to re use code.
...
return PHBufferCalibration(
calibration_name="ph_calibration",
calibrated_on_pioreactor_unit=whoami.get_unit_name(),
created_at=current_utc_datetime(),
curve_data_=[2, 3, 5],
curve_type="poly",
x="Voltage",
y="pH",
recorded_data={"x": [0.1, 0.2, 0.3], "y": [1.0, 2.0, 3.0]},
buffer_solution="10.01",
electrode_type="glass"
)